@automagik/omni 2.260429.5 → 2.260430.2

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.
@@ -0,0 +1,47 @@
1
+ -- Close-contact endpoint audit table.
2
+ --
3
+ -- Records every agent→close-contact event with full payload. Written
4
+ -- synchronously in the /messages/send/close-contact route so no data is
5
+ -- lost. The route also reads this table at close-time to count recent
6
+ -- rows for the same (chat_uuid, outcome) within the configured escalation
7
+ -- window — that count drives the auto-promotion of soft outcomes to hard
8
+ -- terminal (recorded back as `escalated = TRUE` on the new row). See the
9
+ -- design doc at .genie/wishes/559-close-contact/design.md §6 + §8.1 for
10
+ -- the loop-bound proof.
11
+ --
12
+ -- Hot path is the recent-count query, indexed on
13
+ -- (chat_uuid, outcome, sent_at). Per-instance BI feed is indexed on
14
+ -- (instance_id, sent_at). chat_id and agent_id get their own indexes for
15
+ -- support drilldowns.
16
+ --
17
+ -- The `outcome` column is a varchar(32) free string at the DB level; the
18
+ -- TypeScript layer constrains it to the `closeContactOutcomes` const array
19
+ -- in `packages/db/src/schema.ts` (kept in sync with `CloseContactOutcome`
20
+ -- in `@omni/core/events`). No CHECK constraint here so the BI/CRM tooling
21
+ -- can extend the taxonomy without a migration.
22
+
23
+ CREATE TABLE IF NOT EXISTS "close_contact_logs" (
24
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
25
+ "instance_id" uuid REFERENCES "instances"("id") ON DELETE SET NULL,
26
+ "chat_uuid" uuid REFERENCES "chats"("id") ON DELETE SET NULL,
27
+ "chat_id" varchar(255) NOT NULL,
28
+ "to_phone" varchar(100) NOT NULL,
29
+ "text" text NOT NULL,
30
+ "outcome" varchar(32) NOT NULL,
31
+ "reason" text,
32
+ "close_fields" jsonb,
33
+ "agent_id" uuid REFERENCES "agents"("id") ON DELETE SET NULL,
34
+ "external_message_id" varchar(255),
35
+ "escalated" boolean NOT NULL DEFAULT false,
36
+ "sent_at" timestamp with time zone DEFAULT now() NOT NULL,
37
+ "metadata" jsonb
38
+ );
39
+
40
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_outcome_sent_at_idx"
41
+ ON "close_contact_logs" ("chat_uuid", "outcome", "sent_at");
42
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_instance_sent_at_idx"
43
+ ON "close_contact_logs" ("instance_id", "sent_at");
44
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_chat_id_idx"
45
+ ON "close_contact_logs" ("chat_id");
46
+ CREATE INDEX IF NOT EXISTS "close_contact_logs_agent_idx"
47
+ ON "close_contact_logs" ("agent_id");
@@ -232,6 +232,13 @@
232
232
  "when": 1777500000000,
233
233
  "tag": "0032_genie_hosts",
234
234
  "breakpoints": true
235
+ },
236
+ {
237
+ "idx": 33,
238
+ "version": "7",
239
+ "when": 1777590000000,
240
+ "tag": "0033_close_contact_logs",
241
+ "breakpoints": true
235
242
  }
236
243
  ]
237
244
  }
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,CA2S/C"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AA8M5C,wBAAgB,qBAAqB,IAAI,OAAO,CA4T/C"}
package/dist/index.js CHANGED
@@ -5755,6 +5755,7 @@ var init_types = __esm(() => {
5755
5755
  "chat.idle_timeout",
5756
5756
  "chat.handoff_activated",
5757
5757
  "chat.archived",
5758
+ "chat.closed",
5758
5759
  "follow_up.armed",
5759
5760
  "follow_up.fired",
5760
5761
  "follow_up.skipped",
@@ -27730,7 +27731,8 @@ var init_follow_up = __esm(() => {
27730
27731
  "sequence_complete",
27731
27732
  "agent_error",
27732
27733
  "send_failed",
27733
- "session_cleared"
27734
+ "session_cleared",
27735
+ "contact_closed"
27734
27736
  ]);
27735
27737
  FixedScheduleSchema = exports_external.object({
27736
27738
  kind: exports_external.literal("fixed"),
@@ -58589,6 +58591,8 @@ __export(exports_schema, {
58589
58591
  contentTypes: () => contentTypes,
58590
58592
  consumerOffsets: () => consumerOffsets,
58591
58593
  conditionOperators: () => conditionOperators,
58594
+ closeContactOutcomes: () => closeContactOutcomes,
58595
+ closeContactLogs: () => closeContactLogs,
58592
58596
  chatsRelations: () => chatsRelations,
58593
58597
  chats: () => chats,
58594
58598
  chatTypes: () => chatTypes,
@@ -58632,7 +58636,7 @@ __export(exports_schema, {
58632
58636
  accessRules: () => accessRules,
58633
58637
  accessModes: () => accessModes
58634
58638
  });
58635
- var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
58639
+ var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages, omniEvents, handoffLogs, closeContactOutcomes, closeContactLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
58636
58640
  var init_schema2 = __esm(() => {
58637
58641
  init_events();
58638
58642
  init_drizzle_orm();
@@ -59258,6 +59262,28 @@ var init_schema2 = __esm(() => {
59258
59262
  sentAtIdx: index("handoff_logs_sent_at_idx").on(table3.sentAt),
59259
59263
  agentIdx: index("handoff_logs_agent_idx").on(table3.agentId)
59260
59264
  }));
59265
+ closeContactOutcomes = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
59266
+ closeContactLogs = pgTable("close_contact_logs", {
59267
+ id: uuid("id").primaryKey().defaultRandom(),
59268
+ instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "set null" }),
59269
+ chatUuid: uuid("chat_uuid").references(() => chats.id, { onDelete: "set null" }),
59270
+ chatId: varchar("chat_id", { length: 255 }).notNull(),
59271
+ toPhone: varchar("to_phone", { length: 100 }).notNull(),
59272
+ text: text("text").notNull(),
59273
+ outcome: varchar("outcome", { length: 32 }).notNull().$type(),
59274
+ reason: text("reason"),
59275
+ closeFields: jsonb("close_fields").$type(),
59276
+ agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
59277
+ externalMessageId: varchar("external_message_id", { length: 255 }),
59278
+ escalated: boolean("escalated").notNull().default(false),
59279
+ sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(),
59280
+ metadata: jsonb("metadata").$type()
59281
+ }, (table3) => ({
59282
+ chatOutcomeSentAtIdx: index("close_contact_logs_chat_outcome_sent_at_idx").on(table3.chatUuid, table3.outcome, table3.sentAt),
59283
+ instanceSentAtIdx: index("close_contact_logs_instance_sent_at_idx").on(table3.instanceId, table3.sentAt),
59284
+ chatIdIdx: index("close_contact_logs_chat_id_idx").on(table3.chatId),
59285
+ agentIdx: index("close_contact_logs_agent_idx").on(table3.agentId)
59286
+ }));
59261
59287
  accessRules = pgTable("access_rules", {
59262
59288
  id: uuid("id").primaryKey().defaultRandom(),
59263
59289
  instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "cascade" }),
@@ -59892,7 +59918,8 @@ var init_schema2 = __esm(() => {
59892
59918
  "sequence_complete",
59893
59919
  "agent_error",
59894
59920
  "send_failed",
59895
- "session_cleared"
59921
+ "session_cleared",
59922
+ "contact_closed"
59896
59923
  ];
59897
59924
  chatFollowUpState = pgTable("chat_follow_up_state", {
59898
59925
  id: uuid("id").primaryKey().defaultRandom(),
@@ -113953,7 +113980,7 @@ import { fileURLToPath } from "url";
113953
113980
  // package.json
113954
113981
  var package_default = {
113955
113982
  name: "@automagik/omni",
113956
- version: "2.260429.5",
113983
+ version: "2.260430.2",
113957
113984
  description: "LLM-optimized CLI for Omni",
113958
113985
  type: "module",
113959
113986
  bin: {
@@ -122464,8 +122491,77 @@ function createMessagesCommand() {
122464
122491
  error(`Failed to edit message: ${message2}`);
122465
122492
  }
122466
122493
  });
122494
+ messages3.command("close-contact").description("Close a chat terminally (won/lost) or with a soft cooldown (redirected_sac/unqualified/no_response/other)").requiredOption("--instance <id>", "Instance ID").requiredOption("--chat <chatId>", "Chat DB UUID to close").requiredOption("--to <recipient>", "Recipient phone or platform ID").requiredOption("--text <text>", "Farewell message shown to the lead").requiredOption("--outcome <outcome>", "Outcome: won | lost | redirected_sac | unqualified | no_response | other").option("--reason <reason>", "Free-text rationale persisted in close_contact_logs").option("--close-fields <jsonOrPath>", "Structured BI/CRM payload \u2014 inline JSON or path to a JSON file").action(handleCloseContact);
122467
122495
  return messages3;
122468
122496
  }
122497
+ var VALID_CLOSE_OUTCOMES = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
122498
+ async function parseCloseFields(raw2) {
122499
+ const trimmed = raw2.trim();
122500
+ if (trimmed.startsWith("{")) {
122501
+ return JSON.parse(trimmed);
122502
+ }
122503
+ const fs2 = await import("fs/promises");
122504
+ const text3 = await fs2.readFile(trimmed, "utf-8");
122505
+ return JSON.parse(text3);
122506
+ }
122507
+ async function postCloseContact(body) {
122508
+ const _cfg = (await Promise.resolve().then(() => (init_config(), exports_config))).loadConfig();
122509
+ const baseUrl = _cfg.apiUrl ?? "http://localhost:8882";
122510
+ const apiKey = _cfg.apiKey ?? "";
122511
+ const resp = await fetch(`${baseUrl}/api/v2/messages/send/close-contact`, {
122512
+ method: "POST",
122513
+ headers: { "Content-Type": "application/json", "x-api-key": apiKey },
122514
+ body: JSON.stringify(body)
122515
+ });
122516
+ if (!resp.ok) {
122517
+ const err2 = await resp.json().catch(() => ({}));
122518
+ const errMsg = typeof err2.error === "string" ? err2.error : err2.error?.message ?? `API error: ${resp.status}`;
122519
+ throw new Error(errMsg);
122520
+ }
122521
+ const data2 = await resp.json();
122522
+ return data2.data ?? {};
122523
+ }
122524
+ async function buildCloseContactBody(options) {
122525
+ let closeFields;
122526
+ if (options.closeFields) {
122527
+ try {
122528
+ closeFields = await parseCloseFields(options.closeFields);
122529
+ } catch (err2) {
122530
+ error(`Failed to parse --close-fields: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
122531
+ return null;
122532
+ }
122533
+ }
122534
+ const instanceId = await resolveInstanceId(options.instance);
122535
+ const resolvedChatId = await resolveChatId(options.chat);
122536
+ const body = {
122537
+ instanceId,
122538
+ chatId: resolvedChatId,
122539
+ to: options.to,
122540
+ text: options.text,
122541
+ outcome: options.outcome
122542
+ };
122543
+ if (options.reason)
122544
+ body.reason = options.reason;
122545
+ if (closeFields)
122546
+ body.closeFields = closeFields;
122547
+ return body;
122548
+ }
122549
+ async function handleCloseContact(options) {
122550
+ if (!VALID_CLOSE_OUTCOMES.includes(options.outcome)) {
122551
+ error(`Invalid --outcome '${options.outcome}'. Must be one of: ${VALID_CLOSE_OUTCOMES.join(", ")}`);
122552
+ return;
122553
+ }
122554
+ try {
122555
+ const body = await buildCloseContactBody(options);
122556
+ if (!body)
122557
+ return;
122558
+ const result = await postCloseContact(body);
122559
+ const cooldownPart = result.closeUntil ? `, closeUntil=${result.closeUntil}` : "";
122560
+ success(`Chat closed (${options.outcome}): terminal=${result.terminal ?? false}, escalated=${result.escalated ?? false}${cooldownPart}`);
122561
+ } catch (err2) {
122562
+ error(`Failed to close contact: ${err2 instanceof Error ? err2.message : "Unknown error"}`);
122563
+ }
122564
+ }
122469
122565
 
122470
122566
  // src/commands/open.ts
122471
122567
  init_output();
@@ -181,6 +181,7 @@ var init_types2 = __esm(() => {
181
181
  "chat.idle_timeout",
182
182
  "chat.handoff_activated",
183
183
  "chat.archived",
184
+ "chat.closed",
184
185
  "follow_up.armed",
185
186
  "follow_up.fired",
186
187
  "follow_up.skipped",
@@ -22074,7 +22075,8 @@ var init_follow_up = __esm(() => {
22074
22075
  "sequence_complete",
22075
22076
  "agent_error",
22076
22077
  "send_failed",
22077
- "session_cleared"
22078
+ "session_cleared",
22079
+ "contact_closed"
22078
22080
  ]);
22079
22081
  FixedScheduleSchema = exports_external.object({
22080
22082
  kind: exports_external.literal("fixed"),
@@ -224554,7 +224556,7 @@ var init_sentry_scrub = __esm(() => {
224554
224556
  var require_package8 = __commonJS((exports, module) => {
224555
224557
  module.exports = {
224556
224558
  name: "@omni/api",
224557
- version: "2.260429.5",
224559
+ version: "2.260430.2",
224558
224560
  type: "module",
224559
224561
  exports: {
224560
224562
  ".": {
@@ -229981,6 +229983,8 @@ __export(exports_schema, {
229981
229983
  contentTypes: () => contentTypes,
229982
229984
  consumerOffsets: () => consumerOffsets,
229983
229985
  conditionOperators: () => conditionOperators,
229986
+ closeContactOutcomes: () => closeContactOutcomes,
229987
+ closeContactLogs: () => closeContactLogs,
229984
229988
  chatsRelations: () => chatsRelations,
229985
229989
  chats: () => chats,
229986
229990
  chatTypes: () => chatTypes,
@@ -230024,7 +230028,7 @@ __export(exports_schema, {
230024
230028
  accessRules: () => accessRules,
230025
230029
  accessModes: () => accessModes
230026
230030
  });
230027
- var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages2, omniEvents, handoffLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
230031
+ var channelTypes, agentTypes, agentSystems, agentEntityTypes, debounceMode, splitDelayMode, replyFilterMode, agentSessionStrategies, ruleTypes, accessModes, settingValueTypes, apiKeyStatuses, apiKeyProfiles, eventTypes, contentTypes, chatTypes, messageSources, messageTypes, messageStatuses, deliveryStatuses, jobStatuses, providerSchemas, agentProviders, agents, agentRoutes, agentSessions, apiKeys, apiKeyAuditLogs, apiKeysRelations, apiKeyAuditLogsRelations, instances, persons, platformIdentities, conversations, chats, chatParticipants, omniGroups, messages2, omniEvents, handoffLogs, closeContactOutcomes, closeContactLogs, accessRules, globalSettings, settingChangeHistory, batchJobs, syncJobTypes, syncJobs, mediaContent, chatIdMappings, pluginStorage, agentProvidersRelations, agentsRelations, instancesRelations, syncJobsRelations, personsRelations, platformIdentitiesRelations, conversationsRelations, chatsRelations, chatParticipantsRelations, messagesRelations, omniEventsRelations, accessRulesRelations, globalSettingsRelations, settingChangeHistoryRelations, batchJobsRelations, mediaContentRelations, chatIdMappingsRelations, deadLetterStatuses, deadLetterEvents, payloadStorageConfig, payloadStages, eventPayloads, webhookSources, conditionOperators, actionTypes, automationDebounceModes, automations2, automationLogStatuses, automationLogs, consumerOffsets, automationsRelations, automationLogsRelations, triggerLogs, triggerLogsRelations, agentRoutesRelations, agentTaskStatuses, agentTasks, agentTasksRelations, turnStatuses, turnActions, turns, turnsRelations, followUpDisarmReasons, chatFollowUpState, chatFollowUpStateRelations, processedEvents, genieHosts;
230028
230032
  var init_schema2 = __esm(() => {
230029
230033
  init_events();
230030
230034
  init_drizzle_orm();
@@ -230650,6 +230654,28 @@ var init_schema2 = __esm(() => {
230650
230654
  sentAtIdx: index("handoff_logs_sent_at_idx").on(table3.sentAt),
230651
230655
  agentIdx: index("handoff_logs_agent_idx").on(table3.agentId)
230652
230656
  }));
230657
+ closeContactOutcomes = ["won", "lost", "redirected_sac", "unqualified", "no_response", "other"];
230658
+ closeContactLogs = pgTable("close_contact_logs", {
230659
+ id: uuid("id").primaryKey().defaultRandom(),
230660
+ instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "set null" }),
230661
+ chatUuid: uuid("chat_uuid").references(() => chats.id, { onDelete: "set null" }),
230662
+ chatId: varchar("chat_id", { length: 255 }).notNull(),
230663
+ toPhone: varchar("to_phone", { length: 100 }).notNull(),
230664
+ text: text("text").notNull(),
230665
+ outcome: varchar("outcome", { length: 32 }).notNull().$type(),
230666
+ reason: text("reason"),
230667
+ closeFields: jsonb("close_fields").$type(),
230668
+ agentId: uuid("agent_id").references(() => agents.id, { onDelete: "set null" }),
230669
+ externalMessageId: varchar("external_message_id", { length: 255 }),
230670
+ escalated: boolean("escalated").notNull().default(false),
230671
+ sentAt: timestamp("sent_at", { withTimezone: true }).notNull().defaultNow(),
230672
+ metadata: jsonb("metadata").$type()
230673
+ }, (table3) => ({
230674
+ chatOutcomeSentAtIdx: index("close_contact_logs_chat_outcome_sent_at_idx").on(table3.chatUuid, table3.outcome, table3.sentAt),
230675
+ instanceSentAtIdx: index("close_contact_logs_instance_sent_at_idx").on(table3.instanceId, table3.sentAt),
230676
+ chatIdIdx: index("close_contact_logs_chat_id_idx").on(table3.chatId),
230677
+ agentIdx: index("close_contact_logs_agent_idx").on(table3.agentId)
230678
+ }));
230653
230679
  accessRules = pgTable("access_rules", {
230654
230680
  id: uuid("id").primaryKey().defaultRandom(),
230655
230681
  instanceId: uuid("instance_id").references(() => instances.id, { onDelete: "cascade" }),
@@ -231284,7 +231310,8 @@ var init_schema2 = __esm(() => {
231284
231310
  "sequence_complete",
231285
231311
  "agent_error",
231286
231312
  "send_failed",
231287
- "session_cleared"
231313
+ "session_cleared",
231314
+ "contact_closed"
231288
231315
  ];
231289
231316
  chatFollowUpState = pgTable("chat_follow_up_state", {
231290
231317
  id: uuid("id").primaryKey().defaultRandom(),
@@ -240539,7 +240566,7 @@ async function verifySignature3(opts) {
240539
240566
  if (!ok) {
240540
240567
  return { status: "invalid", reason: "signature does not verify under registered pubkey" };
240541
240568
  }
240542
- return { status: "verified", hostId: host.id };
240569
+ return { status: "verified", hostId: host.id, hostScopes: host.scopes };
240543
240570
  }
240544
240571
  function pathFromRequest(url) {
240545
240572
  return `${url.pathname}${url.search}`;
@@ -240576,7 +240603,7 @@ var init_genie_signature = __esm(() => {
240576
240603
  now: Date.now(),
240577
240604
  findHost: async (id) => {
240578
240605
  const host = await services.genieHosts.findById(id);
240579
- return host ? { id: host.id, pubkey: host.pubkey, revokedAt: host.revokedAt } : null;
240606
+ return host ? { id: host.id, pubkey: host.pubkey, revokedAt: host.revokedAt, scopes: host.scopes ?? ["*"] } : null;
240580
240607
  }
240581
240608
  });
240582
240609
  if (outcome.status === "invalid") {
@@ -240595,6 +240622,9 @@ var init_genie_signature = __esm(() => {
240595
240622
  }
240596
240623
  if (outcome.status === "verified" && outcome.hostId) {
240597
240624
  c.set("signedBy", outcome.hostId);
240625
+ if (outcome.hostScopes) {
240626
+ c.set("signedByScopes", outcome.hostScopes);
240627
+ }
240598
240628
  services.genieHosts.touchLastSeen(outcome.hostId).catch((err) => {
240599
240629
  log58.warn("touchLastSeen failed (non-fatal)", { hostId: outcome.hostId, err: String(err) });
240600
240630
  });
@@ -241314,9 +241344,12 @@ var init_scope_enforcer = __esm(() => {
241314
241344
  }
241315
241345
  const method = c.req.method.toUpperCase();
241316
241346
  const path3 = c.req.path;
241347
+ const signedBy = c.get("signedBy");
241348
+ const signedByScopes = c.get("signedByScopes");
241317
241349
  const wildcard = ApiKeyService.scopeAllows(apiKey.scopes, "*");
241350
+ let requiredScope;
241318
241351
  if (!wildcard) {
241319
- const requiredScope = findRequiredScope(method, path3);
241352
+ requiredScope = findRequiredScope(method, path3);
241320
241353
  if (!requiredScope) {
241321
241354
  log60.warn(`DENIED: key=${apiKey.id} route=${method} ${path3} required=UNMAPPED`);
241322
241355
  return c.json({
@@ -241336,6 +241369,31 @@ var init_scope_enforcer = __esm(() => {
241336
241369
  }, 403);
241337
241370
  }
241338
241371
  }
241372
+ if (signedBy && signedByScopes) {
241373
+ const hostWildcard = ApiKeyService.scopeAllows(signedByScopes, "*");
241374
+ if (!hostWildcard) {
241375
+ const needed = requiredScope ?? findRequiredScope(method, path3);
241376
+ if (!needed) {
241377
+ log60.warn(`DENIED: signedBy=${signedBy} route=${method} ${path3} required=UNMAPPED`);
241378
+ return c.json({
241379
+ error: {
241380
+ code: "FORBIDDEN",
241381
+ message: "Insufficient permissions. Route not mapped in scope policy."
241382
+ }
241383
+ }, 403);
241384
+ }
241385
+ if (!ApiKeyService.scopeAllows(signedByScopes, needed)) {
241386
+ log60.warn(`DENIED: signedBy=${signedBy} route=${method} ${path3} required=${needed} host-scopes=${signedByScopes.join(",")}`);
241387
+ return c.json({
241388
+ error: {
241389
+ code: "FORBIDDEN",
241390
+ message: `Insufficient permissions for signing host. Required scope: ${needed}`,
241391
+ host: signedBy
241392
+ }
241393
+ }, 403);
241394
+ }
241395
+ }
241396
+ }
241339
241397
  const body = await safeReadJsonBody(c);
241340
241398
  const targets = extractLockTargets(method, path3, body);
241341
241399
  const instanceResult = enforceInstanceAllowlist(apiKey, targets.instance);
@@ -284644,6 +284702,55 @@ async function dispatchToAgent(services, instance4, msgs, triggerType, channel4,
284644
284702
  });
284645
284703
  await dispatchViaLegacy(services, instance4, msgs, triggerType, channel4, chatId, senderId, personId, senderName, traceId, perThreadExtraContext, senderAgentId);
284646
284704
  }
284705
+ async function applyCloseContactGate(services, chatRecordId, instanceId, chatId, chatSettings) {
284706
+ if (chatSettings?.closed === true) {
284707
+ log94.debug("Chat closed (terminal), skipping dispatch", {
284708
+ instanceId,
284709
+ chatId,
284710
+ outcome: chatSettings.closeOutcome ?? null
284711
+ });
284712
+ return "skip";
284713
+ }
284714
+ if (!chatSettings?.closeUntil)
284715
+ return "pass";
284716
+ const closeUntilMs = new Date(chatSettings.closeUntil).getTime();
284717
+ if (!Number.isFinite(closeUntilMs))
284718
+ return "pass";
284719
+ if (Date.now() < closeUntilMs) {
284720
+ log94.debug("Chat in close cooldown, skipping dispatch", {
284721
+ instanceId,
284722
+ chatId,
284723
+ closeUntil: chatSettings.closeUntil,
284724
+ outcome: chatSettings.closeOutcome ?? null
284725
+ });
284726
+ return "skip";
284727
+ }
284728
+ if (!chatRecordId)
284729
+ return "pass";
284730
+ try {
284731
+ await services.chats.update(chatRecordId, {
284732
+ settings: {
284733
+ ...chatSettings,
284734
+ agentPaused: false,
284735
+ closeUntil: null,
284736
+ agentResumedAt: new Date().toISOString()
284737
+ }
284738
+ });
284739
+ log94.info("Close cooldown expired, agent reopened", {
284740
+ instanceId,
284741
+ chatId,
284742
+ closeOutcome: chatSettings.closeOutcome ?? null
284743
+ });
284744
+ return "reopened";
284745
+ } catch (err) {
284746
+ log94.warn("Failed to flip close cooldown state, deferring dispatch", {
284747
+ instanceId,
284748
+ chatId,
284749
+ error: String(err)
284750
+ });
284751
+ return "skip";
284752
+ }
284753
+ }
284647
284754
  async function processAgentResponse(services, instance4, messages4, triggerType, db2, eventBus) {
284648
284755
  const firstMessage = messages4[0];
284649
284756
  if (!firstMessage)
@@ -284664,7 +284771,12 @@ async function processAgentResponse(services, instance4, messages4, triggerType,
284664
284771
  }
284665
284772
  const chatRecord = await services.chats.findByExternalIdSmart(instance4.id, chatId);
284666
284773
  const chatSettings = chatRecord?.settings;
284667
- const isAgentPaused = chatSettings?.agentPaused === true;
284774
+ const gateResult = await applyCloseContactGate(services, chatRecord?.id ?? null, instance4.id, chatId, chatSettings);
284775
+ if (gateResult === "skip") {
284776
+ ackHandle.remove();
284777
+ return;
284778
+ }
284779
+ const isAgentPaused = chatSettings?.agentPaused === true && gateResult !== "reopened";
284668
284780
  if (isAgentPaused) {
284669
284781
  log94.debug("Agent paused (handoff active), skipping dispatch", { instanceId: instance4.id, chatId });
284670
284782
  ackHandle.remove();
@@ -287129,7 +287241,8 @@ function createServices(db2, eventBus) {
287129
287241
  consumerOffsets: new ConsumerOffsetService(db2),
287130
287242
  followUpLifecycle: new FollowUpLifecycleService(db2, eventBus),
287131
287243
  followUpSweeper: new FollowUpSweeperService(db2, eventBus),
287132
- genieHosts: new GenieHostsService(db2)
287244
+ genieHosts: new GenieHostsService(db2),
287245
+ eventBus
287133
287246
  };
287134
287247
  }
287135
287248
  var init_services = __esm(() => {
@@ -295152,6 +295265,34 @@ var init_chats2 = __esm(() => {
295152
295265
  } catch {}
295153
295266
  return c.json({ success: true, sessionId, sessionStrategy });
295154
295267
  });
295268
+ chatsRoutes.post("/:id/reopen-contact", async (c) => {
295269
+ const id = c.req.param("id");
295270
+ const services = c.get("services");
295271
+ const chat2 = await services.chats.getById(id);
295272
+ if (!chat2)
295273
+ return c.json({ error: "Chat not found" }, 404);
295274
+ if (chat2.instanceId)
295275
+ checkInstanceAccess(c.get("apiKey"), chat2.instanceId);
295276
+ const priorSettings = chat2.settings ?? {};
295277
+ const wasClosed = priorSettings.closed === true || priorSettings.closeUntil != null;
295278
+ await services.chats.update(id, {
295279
+ settings: {
295280
+ ...priorSettings,
295281
+ agentPaused: false,
295282
+ closed: false,
295283
+ closeUntil: null,
295284
+ closeOutcome: null,
295285
+ agentResumedAt: new Date().toISOString()
295286
+ }
295287
+ });
295288
+ return c.json({
295289
+ data: {
295290
+ chatId: id,
295291
+ reopened: true,
295292
+ wasClosed
295293
+ }
295294
+ });
295295
+ });
295155
295296
  });
295156
295297
 
295157
295298
  // ../api/src/routes/v2/context.ts
@@ -300017,6 +300158,38 @@ var init_media = __esm(() => {
300017
300158
  });
300018
300159
  });
300019
300160
 
300161
+ // ../api/src/routes/v2/_close-contact-config.ts
300162
+ function resolveCloseContactConfig(outcome, instanceSettings) {
300163
+ const override = instanceSettings?.closeContactConfig;
300164
+ const def = DEFAULT_CLOSE_CONTACT_CONFIG[outcome];
300165
+ if (!override || typeof override !== "object")
300166
+ return def;
300167
+ const ovr = override[outcome];
300168
+ if (!ovr || typeof ovr !== "object")
300169
+ return def;
300170
+ return {
300171
+ cooldownMs: typeof ovr.cooldownMs === "number" || ovr.cooldownMs === null ? ovr.cooldownMs : def.cooldownMs,
300172
+ escalationThreshold: typeof ovr.escalationThreshold === "number" || ovr.escalationThreshold === null ? ovr.escalationThreshold : def.escalationThreshold,
300173
+ escalationWindowMs: typeof ovr.escalationWindowMs === "number" || ovr.escalationWindowMs === null ? ovr.escalationWindowMs : def.escalationWindowMs
300174
+ };
300175
+ }
300176
+ function isHardTerminalOutcome(outcome) {
300177
+ return outcome === "won" || outcome === "lost";
300178
+ }
300179
+ var HOUR, DAY, DEFAULT_CLOSE_CONTACT_CONFIG;
300180
+ var init__close_contact_config = __esm(() => {
300181
+ HOUR = 60 * 60 * 1000;
300182
+ DAY = 24 * HOUR;
300183
+ DEFAULT_CLOSE_CONTACT_CONFIG = {
300184
+ won: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
300185
+ lost: { cooldownMs: null, escalationThreshold: null, escalationWindowMs: null },
300186
+ redirected_sac: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 7 * DAY },
300187
+ unqualified: { cooldownMs: 7 * DAY, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
300188
+ no_response: { cooldownMs: 48 * HOUR, escalationThreshold: 3, escalationWindowMs: 30 * DAY },
300189
+ other: { cooldownMs: 24 * HOUR, escalationThreshold: 2, escalationWindowMs: 14 * DAY }
300190
+ };
300191
+ });
300192
+
300020
300193
  // ../api/src/routes/v2/messages.ts
300021
300194
  import { existsSync as existsSync8 } from "fs";
300022
300195
  import { join as join24 } from "path";
@@ -300143,6 +300316,27 @@ async function resolveMessageFromRef(services, ref) {
300143
300316
  }
300144
300317
  return found;
300145
300318
  }
300319
+ async function computeCloseContactTerminalState(db2, chatUuid, outcome, auditRowId) {
300320
+ const cfg = resolveCloseContactConfig(outcome, null);
300321
+ if (isHardTerminalOutcome(outcome)) {
300322
+ return { terminal: true, escalated: false, closeUntil: null };
300323
+ }
300324
+ if (cfg.escalationThreshold === null || cfg.escalationWindowMs === null) {
300325
+ const closeUntil2 = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
300326
+ return { terminal: false, escalated: false, closeUntil: closeUntil2 };
300327
+ }
300328
+ const windowStart = new Date(Date.now() - cfg.escalationWindowMs);
300329
+ const recent = await db2.select({ count: sql`count(*)::int` }).from(closeContactLogs).where(and2(eq(closeContactLogs.chatUuid, chatUuid), eq(closeContactLogs.outcome, outcome), gte(closeContactLogs.sentAt, windowStart)));
300330
+ const recentCount = Number(recent[0]?.count ?? 0);
300331
+ if (recentCount >= cfg.escalationThreshold) {
300332
+ if (auditRowId) {
300333
+ await db2.update(closeContactLogs).set({ escalated: true }).where(eq(closeContactLogs.id, auditRowId));
300334
+ }
300335
+ return { terminal: true, escalated: true, closeUntil: null };
300336
+ }
300337
+ const closeUntil = cfg.cooldownMs !== null ? new Date(Date.now() + cfg.cooldownMs) : null;
300338
+ return { terminal: false, escalated: false, closeUntil };
300339
+ }
300146
300340
  async function verifyMessageInstanceOwnership(services, message2, instanceId) {
300147
300341
  if (!message2.chatId)
300148
300342
  return;
@@ -300156,19 +300350,21 @@ async function verifyMessageInstanceOwnership(services, message2, instanceId) {
300156
300350
  });
300157
300351
  }
300158
300352
  }
300159
- var log105, mediaDownloadLog, messagesRoutes, UUID_REGEX, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema13, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, sendHandoffSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
300353
+ var log105, mediaDownloadLog, messagesRoutes, UUID_REGEX, MessageSourceSchema, MessageTypeSchema, MessageStatusSchema, DeliveryStatusSchema, listQuerySchema13, createMessageSchema, updateMessageSchema, recordEditSchema, addReactionSchema, removeReactionSchema, updateDeliveryStatusSchema, MentionSchema, sendTextSchema, sendMediaSchema, sendReactionSchema, sendStickerSchema, sendContactSchema, sendLocationSchema, sendHandoffSchema, sendCloseContactSchema, messageRefSchema, _mediaStorageForDownload = null, sendTtsSchema, forwardMessageSchema, sendPresenceSchema, markMessageReadSchema, markBatchReadSchema, sendPollSchema, sendEmbedSchema, editMessageChannelSchema, deleteMessageChannelSchema, starMessageSchema;
300160
300354
  var init_messages5 = __esm(() => {
300161
300355
  init_dist6();
300162
300356
  init_src2();
300163
300357
  init_src();
300164
300358
  init_src5();
300165
300359
  init_esm5();
300360
+ init_drizzle_orm();
300166
300361
  init_dist2();
300167
300362
  init_zod();
300168
300363
  init_sentry_scrub();
300169
300364
  init_date_query();
300170
300365
  init_api_keys();
300171
300366
  init_media_storage();
300367
+ init__close_contact_config();
300172
300368
  log105 = createLogger("routes:messages");
300173
300369
  mediaDownloadLog = createLogger("routes:messages:media-download");
300174
300370
  messagesRoutes = new Hono2;
@@ -300330,6 +300526,15 @@ var init_messages5 = __esm(() => {
300330
300526
  extraInfo: exports_external.string().optional().describe("Free-text briefing (legacy \u2014 prefer dadosLead)"),
300331
300527
  handoffFields: exports_external.record(exports_external.unknown()).optional().describe("Structured fields for Gupshup flow variables (e.g. nome, cidade, temperatura_lead)")
300332
300528
  });
300529
+ sendCloseContactSchema = exports_external.object({
300530
+ instanceId: exports_external.string().uuid().describe("Instance ID \u2014 close-contact native send is Gupshup-only in v1"),
300531
+ chatId: exports_external.string().min(1).describe("Chat DB UUID to mark as closed"),
300532
+ to: exports_external.string().min(1).describe("Recipient phone or platform ID"),
300533
+ text: exports_external.string().min(1).describe("Farewell message shown to the lead"),
300534
+ outcome: exports_external.enum(["won", "lost", "redirected_sac", "unqualified", "no_response", "other"]).describe("Drives terminal/cooldown/escalation logic and BI/audit trail"),
300535
+ reason: exports_external.string().optional().describe("Free-text rationale persisted in close_contact_logs"),
300536
+ closeFields: exports_external.record(exports_external.unknown()).optional().describe("Structured BI/CRM payload \u2014 forwarded to Gupshup native send when supported")
300537
+ });
300333
300538
  messagesRoutes.get("/", zValidator("query", listQuerySchema13), async (c) => {
300334
300539
  const query = c.req.valid("query");
300335
300540
  const services = c.get("services");
@@ -301048,6 +301253,109 @@ var init_messages5 = __esm(() => {
301048
301253
  }
301049
301254
  }, 201);
301050
301255
  });
301256
+ messagesRoutes.post("/send/close-contact", zValidator("json", sendCloseContactSchema), async (c) => {
301257
+ const data = c.req.valid("json");
301258
+ const services = c.get("services");
301259
+ const db2 = c.get("db");
301260
+ const channelRegistry2 = c.get("channelRegistry");
301261
+ checkInstanceAccess2(c.get("apiKey"), data.instanceId);
301262
+ const instance4 = await services.instances.getById(data.instanceId);
301263
+ if (!channelRegistry2) {
301264
+ throw new OmniError({
301265
+ code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
301266
+ message: "Channel registry not available",
301267
+ recoverable: false
301268
+ });
301269
+ }
301270
+ const plugin7 = channelRegistry2.get(instance4.channel);
301271
+ if (!plugin7) {
301272
+ throw new OmniError({
301273
+ code: ERROR_CODES.CHANNEL_NOT_CONNECTED,
301274
+ message: `No plugin found for channel: ${instance4.channel}`,
301275
+ context: { channelType: instance4.channel },
301276
+ recoverable: false
301277
+ });
301278
+ }
301279
+ const resolvedTo = await resolveRecipient(data.to, instance4.channel, services);
301280
+ const hasNativeClose = plugin7.capabilities?.canCloseContact === true;
301281
+ const outcome = data.outcome;
301282
+ let channelSendResult = null;
301283
+ if (hasNativeClose) {
301284
+ const outgoingMessage = {
301285
+ to: resolvedTo,
301286
+ content: { type: "text", text: data.text },
301287
+ metadata: {
301288
+ isCloseContact: true,
301289
+ closeReason: data.reason,
301290
+ closeOutcome: outcome,
301291
+ closeFields: data.closeFields
301292
+ }
301293
+ };
301294
+ channelSendResult = await plugin7.sendMessage(data.instanceId, outgoingMessage);
301295
+ handleSendResult(channelSendResult, {
301296
+ channelType: instance4.channel,
301297
+ instanceId: data.instanceId,
301298
+ operation: "send close-contact"
301299
+ });
301300
+ }
301301
+ const [auditRow] = await db2.insert(closeContactLogs).values({
301302
+ instanceId: data.instanceId,
301303
+ chatUuid: data.chatId,
301304
+ chatId: resolvedTo,
301305
+ toPhone: resolvedTo,
301306
+ text: data.text,
301307
+ outcome,
301308
+ reason: data.reason ?? null,
301309
+ closeFields: data.closeFields ?? null,
301310
+ agentId: instance4.agentId ?? null,
301311
+ externalMessageId: channelSendResult?.messageId ?? null,
301312
+ escalated: false,
301313
+ sentAt: new Date,
301314
+ metadata: {
301315
+ instanceChannel: instance4.channel,
301316
+ channelCloseSupported: hasNativeClose
301317
+ }
301318
+ }).returning();
301319
+ const { terminal, escalated, closeUntil } = await computeCloseContactTerminalState(db2, data.chatId, outcome, auditRow?.id ?? null);
301320
+ const closedAt = new Date;
301321
+ await services.chats.update(data.chatId, {
301322
+ settings: {
301323
+ agentPaused: true,
301324
+ closed: terminal,
301325
+ closeUntil: closeUntil?.toISOString() ?? null,
301326
+ closeOutcome: outcome
301327
+ }
301328
+ });
301329
+ await services.followUpLifecycle.disarm({
301330
+ chatId: data.chatId,
301331
+ instanceId: data.instanceId,
301332
+ reason: "contact_closed"
301333
+ });
301334
+ if (services.eventBus) {
301335
+ const payload = {
301336
+ chatId: data.chatId,
301337
+ instanceId: data.instanceId,
301338
+ agentId: instance4.agentId ?? null,
301339
+ outcome,
301340
+ reason: data.reason ?? null,
301341
+ escalated: terminal,
301342
+ closedFields: data.closeFields ?? null,
301343
+ closedAt: closedAt.toISOString()
301344
+ };
301345
+ services.eventBus.publish("chat.closed", payload, { instanceId: data.instanceId }).catch((err) => log105.debug("Failed to publish chat.closed", { error: String(err) }));
301346
+ }
301347
+ return c.json({
301348
+ data: {
301349
+ messageId: channelSendResult?.messageId ?? null,
301350
+ status: "closed",
301351
+ terminal,
301352
+ closeUntil: closeUntil?.toISOString() ?? null,
301353
+ escalated,
301354
+ outcome,
301355
+ timestamp: channelSendResult?.timestamp ?? closedAt.getTime()
301356
+ }
301357
+ }, 201);
301358
+ });
301051
301359
  messagesRoutes.get("/tts/voices", async (c) => {
301052
301360
  const services = c.get("services");
301053
301361
  const voices = await services.tts.listVoices();
@@ -304570,7 +304878,22 @@ async function setupFollowUpHooks(eventBus, services) {
304570
304878
  retryDelayMs: 500,
304571
304879
  startFrom: "new"
304572
304880
  });
304573
- log109.info("Follow-up hooks active (message.sent/received, chat.handoff_activated, chat.archived)");
304881
+ await eventBus.subscribe("chat.closed", async (event) => {
304882
+ const payload = event.payload;
304883
+ await services.followUpLifecycle.disarm({
304884
+ chatId: payload.chatId,
304885
+ instanceId: payload.instanceId,
304886
+ agentId: payload.agentId ?? null,
304887
+ reason: "contact_closed"
304888
+ });
304889
+ }, {
304890
+ durable: "follow-up-hooks-closed",
304891
+ queue: "follow-up-hooks",
304892
+ maxRetries: 2,
304893
+ retryDelayMs: 500,
304894
+ startFrom: "new"
304895
+ });
304896
+ log109.info("Follow-up hooks active (message.sent/received, chat.handoff_activated, chat.archived, chat.closed)");
304574
304897
  } catch (error2) {
304575
304898
  log109.error("Failed to set up follow-up hooks", { error: String(error2) });
304576
304899
  }
@@ -309561,6 +309884,7 @@ var GUPSHUP_CAPABILITIES = {
309561
309884
  canReplyToMessage: true,
309562
309885
  canForwardMessage: false,
309563
309886
  canHandoff: true,
309887
+ canCloseContact: true,
309564
309888
  canSendContact: false,
309565
309889
  canSendLocation: true,
309566
309890
  canSendSticker: true,
@@ -310028,6 +310352,18 @@ function extractContent2(msg) {
310028
310352
  return null;
310029
310353
  }
310030
310354
 
310355
+ // ../channel-gupshup/src/senders/close-contact.ts
310356
+ var GUPSHUP_CLOSE_MSG_TYPE = "CLOSING";
310357
+ async function sendCloseContact(client, to, text, closeReason, closeOutcome, closeFields) {
310358
+ return client.send(to, {
310359
+ type: GUPSHUP_CLOSE_MSG_TYPE,
310360
+ text,
310361
+ close_reason: closeReason,
310362
+ close_outcome: closeOutcome,
310363
+ close_fields: closeFields
310364
+ });
310365
+ }
310366
+
310031
310367
  // ../channel-gupshup/src/senders/handoff.ts
310032
310368
  async function sendHandoff(client, to, text, dadosLead, motivoHandoff, handoffFields) {
310033
310369
  return client.send(to, {
@@ -310100,6 +310436,12 @@ async function dispatchContent(client, dest, message2) {
310100
310436
  const handoffFields = meta.handoffFields;
310101
310437
  return sendHandoff(client, dest, content.text ?? "", dadosLead, motivoHandoff, handoffFields);
310102
310438
  }
310439
+ if (meta?.isCloseContact === true) {
310440
+ const closeReason = meta.closeReason;
310441
+ const closeOutcome = meta.closeOutcome;
310442
+ const closeFields = meta.closeFields;
310443
+ return sendCloseContact(client, dest, content.text ?? "", closeReason, closeOutcome, closeFields);
310444
+ }
310103
310445
  if (content.type === "text") {
310104
310446
  return sendText(client, dest, content.text ?? "");
310105
310447
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/omni",
3
- "version": "2.260429.5",
3
+ "version": "2.260430.2",
4
4
  "description": "LLM-optimized CLI for Omni",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,15 +50,15 @@
50
50
  "qrcode-terminal": "^0.12.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@omni/api": "2.260429.4",
54
- "@omni/channel-discord": "2.260429.4",
55
- "@omni/channel-gupshup": "2.260429.4",
56
- "@omni/channel-sdk": "2.260429.4",
57
- "@omni/channel-slack": "2.260429.4",
58
- "@omni/channel-telegram": "2.260429.4",
59
- "@omni/channel-whatsapp": "2.260429.4",
60
- "@omni/core": "2.260429.4",
61
- "@omni/sdk": "2.260429.4",
53
+ "@omni/api": "2.260430.1",
54
+ "@omni/channel-discord": "2.260430.1",
55
+ "@omni/channel-gupshup": "2.260430.1",
56
+ "@omni/channel-sdk": "2.260430.1",
57
+ "@omni/channel-slack": "2.260430.1",
58
+ "@omni/channel-telegram": "2.260430.1",
59
+ "@omni/channel-whatsapp": "2.260430.1",
60
+ "@omni/core": "2.260430.1",
61
+ "@omni/sdk": "2.260430.1",
62
62
  "@types/node": "^22.10.3",
63
63
  "@types/qrcode-terminal": "^0.12.2",
64
64
  "typescript": "^5.7.3"