iris-chatbot 5.2.0 → 5.3.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.
@@ -1,9 +1,17 @@
1
1
  import { nanoid } from "nanoid";
2
2
 
3
- export type ApprovalDecision = "approve" | "deny" | "timeout";
3
+ export type ApprovalDecision = "approve" | "deny" | "timeout" | "supersede";
4
+ export type ApprovalSource = "user" | "system";
5
+ export type ApprovalReasonCode = "user_cancel" | "internal_replace" | "timeout" | "other";
6
+ export type ApprovalResolution = {
7
+ decision: ApprovalDecision;
8
+ args?: Record<string, unknown>;
9
+ source?: ApprovalSource;
10
+ reasonCode?: ApprovalReasonCode;
11
+ };
4
12
 
5
13
  type PendingApproval = {
6
- resolve: (decision: ApprovalDecision) => void;
14
+ resolve: (resolution: ApprovalResolution) => void;
7
15
  timeout: NodeJS.Timeout;
8
16
  };
9
17
 
@@ -29,8 +37,8 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
29
37
  const approvalId = nanoid();
30
38
  const timeoutMs = params?.timeoutMs ?? 5 * 60_000;
31
39
 
32
- let resolveRef: ((decision: ApprovalDecision) => void) | null = null;
33
- const promise = new Promise<ApprovalDecision>((resolve) => {
40
+ let resolveRef: ((resolution: ApprovalResolution) => void) | null = null;
41
+ const promise = new Promise<ApprovalResolution>((resolve) => {
34
42
  resolveRef = resolve;
35
43
  });
36
44
 
@@ -40,13 +48,13 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
40
48
  return;
41
49
  }
42
50
  store.pendingApprovals.delete(approvalId);
43
- pending.resolve("timeout");
51
+ pending.resolve({ decision: "timeout", source: "system", reasonCode: "timeout" });
44
52
  }, timeoutMs);
45
53
 
46
54
  store.pendingApprovals.set(approvalId, {
47
- resolve: (decision) => {
55
+ resolve: (resolution) => {
48
56
  clearTimeout(timeout);
49
- resolveRef?.(decision);
57
+ resolveRef?.(resolution);
50
58
  },
51
59
  timeout,
52
60
  });
@@ -59,7 +67,9 @@ export function createApprovalRequest(params?: { timeoutMs?: number }) {
59
67
 
60
68
  export function resolveApprovalDecision(
61
69
  approvalId: string,
62
- decision: "approve" | "deny",
70
+ decision: "approve" | "deny" | "supersede",
71
+ args?: Record<string, unknown>,
72
+ meta?: { source?: ApprovalSource; reasonCode?: ApprovalReasonCode },
63
73
  ): boolean {
64
74
  const store = getApprovalStore();
65
75
  const pending = store.pendingApprovals.get(approvalId);
@@ -69,7 +79,12 @@ export function resolveApprovalDecision(
69
79
 
70
80
  store.pendingApprovals.delete(approvalId);
71
81
  clearTimeout(pending.timeout);
72
- pending.resolve(decision);
82
+ pending.resolve({
83
+ decision,
84
+ args,
85
+ source: meta?.source ?? "user",
86
+ reasonCode: meta?.reasonCode,
87
+ });
73
88
  return true;
74
89
  }
75
90
 
@@ -39,6 +39,20 @@ type ResolvedMessageRecipient = {
39
39
  source: "contact" | "direct";
40
40
  };
41
41
 
42
+ type ResolvedMailRecipient = {
43
+ input: string;
44
+ name: string | null;
45
+ address: string;
46
+ source: "contact" | "direct";
47
+ };
48
+
49
+ export type ContactAutocompleteMode = "message" | "email";
50
+ export type ContactAutocompleteSuggestion = {
51
+ name: string;
52
+ value: string;
53
+ detail: string;
54
+ };
55
+
42
56
  const APPLESCRIPT_TIMEOUT_MS = 45_000;
43
57
  const CONTACT_RECORD_DELIMITER = String.fromCharCode(30);
44
58
  const CONTACT_FIELD_DELIMITER = String.fromCharCode(31);
@@ -214,6 +228,18 @@ function selectPreferredMessageHandle(contact: ContactRecord): string | null {
214
228
  return null;
215
229
  }
216
230
 
231
+ function selectPreferredEmail(contact: ContactRecord): string | null {
232
+ const emails = dedupeList(
233
+ contact.emails
234
+ .map((value) => value.trim())
235
+ .filter((value) => EMAIL_REGEX.test(value)),
236
+ );
237
+ if (emails.length > 0) {
238
+ return emails[0];
239
+ }
240
+ return null;
241
+ }
242
+
217
243
  function formatContactOption(contact: ContactRecord): string {
218
244
  const name = contactDisplayName(contact);
219
245
  const handle = selectPreferredMessageHandle(contact);
@@ -516,6 +542,51 @@ async function getContactsMatchingQuery(query: string, signal?: AbortSignal): Pr
516
542
  }
517
543
  }
518
544
 
545
+ export async function autocompleteContacts(params: {
546
+ query: string;
547
+ mode: ContactAutocompleteMode;
548
+ signal?: AbortSignal;
549
+ }): Promise<ContactAutocompleteSuggestion[]> {
550
+ const query = params.query.trim();
551
+ if (!query) {
552
+ return [];
553
+ }
554
+ const contacts = await getContactsMatchingQuery(query, params.signal);
555
+ const matches = findContactMatches(query, contacts);
556
+ const seen = new Set<string>();
557
+ const suggestions: ContactAutocompleteSuggestion[] = [];
558
+
559
+ for (const match of matches) {
560
+ const contact = match.contact;
561
+ const name = contactDisplayName(contact);
562
+ const phones = dedupeList(contact.phones.map(normalizePhoneHandle).filter(Boolean));
563
+ const emails = dedupeList(
564
+ contact.emails
565
+ .map((value) => value.trim())
566
+ .filter((value) => EMAIL_REGEX.test(value)),
567
+ );
568
+ const orderedHandles =
569
+ params.mode === "message"
570
+ ? [...phones, ...emails]
571
+ : emails;
572
+ const value = orderedHandles[0];
573
+ if (!value || seen.has(value)) {
574
+ continue;
575
+ }
576
+ seen.add(value);
577
+ suggestions.push({
578
+ name,
579
+ value,
580
+ detail: value,
581
+ });
582
+ if (suggestions.length >= 8) {
583
+ break;
584
+ }
585
+ }
586
+
587
+ return suggestions;
588
+ }
589
+
519
590
  async function resolveMessageRecipients(
520
591
  requestedRecipients: string[],
521
592
  signal?: AbortSignal,
@@ -540,34 +611,7 @@ async function resolveMessageRecipients(
540
611
  continue;
541
612
  }
542
613
 
543
- const queryCandidates = dedupeList([
544
- entry,
545
- ...normalizeNameForMatch(entry)
546
- .split(" ")
547
- .filter((token) => token.length >= 3),
548
- ]);
549
- const candidateContacts: ContactRecord[] = [];
550
- const candidateFingerprints = new Set<string>();
551
- for (const query of queryCandidates) {
552
- const contacts = await getContactsMatchingQuery(query, signal);
553
- for (const contact of contacts) {
554
- const fingerprint = [
555
- contact.fullName,
556
- contact.firstName,
557
- contact.lastName,
558
- contact.phones.join("|"),
559
- contact.emails.join("|"),
560
- ].join("||");
561
- if (candidateFingerprints.has(fingerprint)) {
562
- continue;
563
- }
564
- candidateFingerprints.add(fingerprint);
565
- candidateContacts.push(contact);
566
- }
567
- if (candidateContacts.length >= 24) {
568
- break;
569
- }
570
- }
614
+ const candidateContacts = await collectCandidateContacts(entry, signal);
571
615
 
572
616
  const matches = findContactMatches(entry, candidateContacts);
573
617
  if (matches.length === 0) {
@@ -591,6 +635,85 @@ async function resolveMessageRecipients(
591
635
  return resolved;
592
636
  }
593
637
 
638
+ async function collectCandidateContacts(entry: string, signal?: AbortSignal): Promise<ContactRecord[]> {
639
+ const queryCandidates = dedupeList([
640
+ entry,
641
+ ...normalizeNameForMatch(entry)
642
+ .split(" ")
643
+ .filter((token) => token.length >= 3),
644
+ ]);
645
+ const candidateContacts: ContactRecord[] = [];
646
+ const candidateFingerprints = new Set<string>();
647
+ for (const query of queryCandidates) {
648
+ const contacts = await getContactsMatchingQuery(query, signal);
649
+ for (const contact of contacts) {
650
+ const fingerprint = [
651
+ contact.fullName,
652
+ contact.firstName,
653
+ contact.lastName,
654
+ contact.phones.join("|"),
655
+ contact.emails.join("|"),
656
+ ].join("||");
657
+ if (candidateFingerprints.has(fingerprint)) {
658
+ continue;
659
+ }
660
+ candidateFingerprints.add(fingerprint);
661
+ candidateContacts.push(contact);
662
+ }
663
+ if (candidateContacts.length >= 24) {
664
+ break;
665
+ }
666
+ }
667
+ return candidateContacts;
668
+ }
669
+
670
+ async function resolveMailRecipients(
671
+ requestedRecipients: string[],
672
+ signal?: AbortSignal,
673
+ ): Promise<ResolvedMailRecipient[]> {
674
+ const recipients = requestedRecipients
675
+ .map((entry) => entry.trim())
676
+ .filter(Boolean);
677
+ if (recipients.length === 0) {
678
+ throw new Error("Missing required array field: to");
679
+ }
680
+
681
+ const resolved: ResolvedMailRecipient[] = [];
682
+ for (const entry of recipients) {
683
+ const direct = entry.trim();
684
+ if (EMAIL_REGEX.test(direct)) {
685
+ resolved.push({
686
+ input: entry,
687
+ name: null,
688
+ address: direct,
689
+ source: "direct",
690
+ });
691
+ continue;
692
+ }
693
+
694
+ const candidateContacts = await collectCandidateContacts(entry, signal);
695
+ const matches = findContactMatches(entry, candidateContacts);
696
+ if (matches.length === 0) {
697
+ throw notFoundContactError(entry, candidateContacts);
698
+ }
699
+
700
+ const matchedContact = resolveContactFromMatches(entry, matches);
701
+ const address = selectPreferredEmail(matchedContact);
702
+ if (!address) {
703
+ throw new Error(
704
+ `Contact "${contactDisplayName(matchedContact)}" has no email address available for Mail.`,
705
+ );
706
+ }
707
+ resolved.push({
708
+ input: entry,
709
+ name: contactDisplayName(matchedContact),
710
+ address,
711
+ source: "contact",
712
+ });
713
+ }
714
+ return resolved;
715
+ }
716
+
594
717
  async function runMessagesSendScript(
595
718
  handles: string[],
596
719
  body: string,
@@ -674,15 +797,25 @@ async function runAppleScript(
674
797
  async function runMailCreateDraft(input: unknown, context: ToolExecutionContext) {
675
798
  ensureMacOS("Mail automation");
676
799
  const payload = asObject(input) as MailInput;
677
- const to = asStringArray(payload.to, "to");
800
+ const requestedTo = asStringArray(payload.to, "to");
678
801
  const subject = asString(payload.subject, "subject");
679
802
  const body = asString(payload.body, "body");
680
- const cc = Array.isArray(payload.cc)
803
+ const requestedCc = Array.isArray(payload.cc)
681
804
  ? payload.cc.filter((item): item is string => typeof item === "string" && item.trim() !== "")
682
805
  : [];
683
806
  const attachments = Array.isArray(payload.attachments)
684
807
  ? payload.attachments.filter((item): item is string => typeof item === "string" && item.trim() !== "")
685
808
  : [];
809
+ const resolvedTo = await resolveMailRecipients(requestedTo, context.signal);
810
+ const resolvedCc = requestedCc.length > 0
811
+ ? await resolveMailRecipients(requestedCc, context.signal)
812
+ : [];
813
+ const to = dedupeList(resolvedTo.map((recipient) => recipient.address));
814
+ const cc = dedupeList(
815
+ resolvedCc
816
+ .map((recipient) => recipient.address)
817
+ .filter((address) => !to.includes(address)),
818
+ );
686
819
 
687
820
  const resolvedAttachments: string[] = [];
688
821
  for (const attachment of attachments) {
@@ -703,6 +836,10 @@ async function runMailCreateDraft(input: unknown, context: ToolExecutionContext)
703
836
  cc,
704
837
  subject,
705
838
  hasBody: Boolean(body),
839
+ recipients: {
840
+ to: resolvedTo,
841
+ cc: resolvedCc,
842
+ },
706
843
  attachments: resolvedAttachments,
707
844
  };
708
845
  }
@@ -729,7 +866,17 @@ async function runMailCreateDraft(input: unknown, context: ToolExecutionContext)
729
866
  'end run';
730
867
 
731
868
  await runAppleScript(script, [to.join("\n"), cc.join("\n"), subject, body], context.signal);
732
- return { drafted: true, to, cc, subject, attachments: resolvedAttachments.length };
869
+ return {
870
+ drafted: true,
871
+ to,
872
+ cc,
873
+ subject,
874
+ recipients: {
875
+ to: resolvedTo,
876
+ cc: resolvedCc,
877
+ },
878
+ attachments: resolvedAttachments.length,
879
+ };
733
880
  }
734
881
 
735
882
  async function runMailSend(input: unknown, context: ToolExecutionContext) {
@@ -56,6 +56,7 @@ export type LocalToolsSettings = {
56
56
  enabled: boolean;
57
57
  approvalMode: ApprovalMode;
58
58
  safetyProfile: SafetyProfile;
59
+ preSendDraftReview: boolean;
59
60
  allowedRoots: string[];
60
61
  enableNotes: boolean;
61
62
  enableApps: boolean;
@@ -81,6 +82,7 @@ export const DEFAULT_LOCAL_TOOLS_SETTINGS: LocalToolsSettings = {
81
82
  enabled: true,
82
83
  approvalMode: "trusted_auto",
83
84
  safetyProfile: "balanced",
85
+ preSendDraftReview: true,
84
86
  allowedRoots: [...DEFAULT_LOCAL_TOOL_ROOTS],
85
87
  enableNotes: true,
86
88
  enableApps: true,
@@ -196,7 +198,7 @@ export type ToolEvent = {
196
198
  createdAt: number;
197
199
  };
198
200
 
199
- export type ToolApprovalStatus = "requested" | "approved" | "denied" | "timeout";
201
+ export type ToolApprovalStatus = "requested" | "approved" | "denied" | "timeout" | "superseded";
200
202
 
201
203
  export type ToolApproval = {
202
204
  id: string;
@@ -238,6 +240,14 @@ export type ChatRequest = {
238
240
  threadId?: string;
239
241
  conversationId?: string;
240
242
  assistantMessageId?: string;
243
+ forceToolRouting?: boolean;
244
+ draftEditContext?: {
245
+ toolName: "mail_send" | "messages_send";
246
+ to: string[];
247
+ cc?: string[];
248
+ subject?: string;
249
+ body: string;
250
+ };
241
251
  };
242
252
  };
243
253
 
@@ -281,7 +291,7 @@ export type ApprovalResolvedEvent = {
281
291
  type: "approval_resolved";
282
292
  approvalId: string;
283
293
  callId: string;
284
- decision: "approve" | "deny" | "timeout";
294
+ decision: "approve" | "deny" | "timeout" | "supersede";
285
295
  message?: string;
286
296
  };
287
297