superceo 0.3.11 → 0.3.12

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 (79) hide show
  1. package/dist/index.js +604 -74
  2. package/dist/index.js.map +4 -4
  3. package/dist/ui-dist/assets/{_basePickBy-C249Trf_.js → _basePickBy-Rxb9gBzf.js} +1 -1
  4. package/dist/ui-dist/assets/{_baseUniq-CTvApBUJ.js → _baseUniq-BAaLJmda.js} +1 -1
  5. package/dist/ui-dist/assets/{arc-BYA6teu5.js → arc-DyBpKcFk.js} +1 -1
  6. package/dist/ui-dist/assets/{architectureDiagram-VXUJARFQ-eXB6QfnC.js → architectureDiagram-VXUJARFQ-DnEQAiKY.js} +1 -1
  7. package/dist/ui-dist/assets/{blockDiagram-VD42YOAC-DMIzX_AO.js → blockDiagram-VD42YOAC-CDBBeg4n.js} +1 -1
  8. package/dist/ui-dist/assets/{c4Diagram-YG6GDRKO-C80xMgwJ.js → c4Diagram-YG6GDRKO-BaKiTPWH.js} +1 -1
  9. package/dist/ui-dist/assets/channel-DWDjs7Td.js +1 -0
  10. package/dist/ui-dist/assets/{chunk-4BX2VUAB-DKXvVYvA.js → chunk-4BX2VUAB-DBYSE0jd.js} +1 -1
  11. package/dist/ui-dist/assets/{chunk-55IACEB6-D7O50kd1.js → chunk-55IACEB6-B2SGBd9r.js} +1 -1
  12. package/dist/ui-dist/assets/{chunk-B4BG7PRW-BU5sraLo.js → chunk-B4BG7PRW-BmMNKP5-.js} +1 -1
  13. package/dist/ui-dist/assets/{chunk-DI55MBZ5-DFWgzS-Q.js → chunk-DI55MBZ5-B6smpJGv.js} +1 -1
  14. package/dist/ui-dist/assets/{chunk-FMBD7UC4-Ca_9o5bl.js → chunk-FMBD7UC4-B8uduxyR.js} +1 -1
  15. package/dist/ui-dist/assets/{chunk-QN33PNHL-0UwO6Gni.js → chunk-QN33PNHL-YCxAvbU_.js} +1 -1
  16. package/dist/ui-dist/assets/{chunk-QZHKN3VN-DGWw0Cd_.js → chunk-QZHKN3VN-7rzCBJuM.js} +1 -1
  17. package/dist/ui-dist/assets/{chunk-TZMSLE5B-D1laE8TU.js → chunk-TZMSLE5B-BCPoM1RZ.js} +1 -1
  18. package/dist/ui-dist/assets/classDiagram-2ON5EDUG-cZtrh3Jb.js +1 -0
  19. package/dist/ui-dist/assets/classDiagram-v2-WZHVMYZB-cZtrh3Jb.js +1 -0
  20. package/dist/ui-dist/assets/clone-CbnB17HW.js +1 -0
  21. package/dist/ui-dist/assets/{cose-bilkent-S5V4N54A-y8dU8mjf.js → cose-bilkent-S5V4N54A-CmWVX5Ss.js} +1 -1
  22. package/dist/ui-dist/assets/{dagre-6UL2VRFP-B6dcqfp3.js → dagre-6UL2VRFP-Ce5_bTvK.js} +1 -1
  23. package/dist/ui-dist/assets/{diagram-PSM6KHXK-IeQCXN28.js → diagram-PSM6KHXK-717OCB12.js} +1 -1
  24. package/dist/ui-dist/assets/{diagram-QEK2KX5R-BnK9g2ZV.js → diagram-QEK2KX5R-exBLiSKc.js} +1 -1
  25. package/dist/ui-dist/assets/{diagram-S2PKOQOG-E0gOOmgp.js → diagram-S2PKOQOG-D-I7HtpT.js} +1 -1
  26. package/dist/ui-dist/assets/{erDiagram-Q2GNP2WA-L6tsRO7_.js → erDiagram-Q2GNP2WA-j6PHZESx.js} +1 -1
  27. package/dist/ui-dist/assets/{flowDiagram-NV44I4VS-DSCF8l5y.js → flowDiagram-NV44I4VS-BGG4niPV.js} +1 -1
  28. package/dist/ui-dist/assets/{ganttDiagram-JELNMOA3-BMi4KHYr.js → ganttDiagram-JELNMOA3-C1BEo9cO.js} +1 -1
  29. package/dist/ui-dist/assets/{gitGraphDiagram-V2S2FVAM-VWUlait1.js → gitGraphDiagram-V2S2FVAM-BohL-82C.js} +1 -1
  30. package/dist/ui-dist/assets/{graph-BkLhHGvA.js → graph-Bnq593xE.js} +1 -1
  31. package/dist/ui-dist/assets/{index-t-1Meejd.js → index-77jd8y5K.js} +1 -1
  32. package/dist/ui-dist/assets/{index-DAqMGxyt.js → index-8PniqIjO.js} +1 -1
  33. package/dist/ui-dist/assets/{index-8z_amSP8.js → index-B09qV1Ec.js} +1 -1
  34. package/dist/ui-dist/assets/{index-CsxGT9oF.js → index-B1NPpgJm.js} +1 -1
  35. package/dist/ui-dist/assets/{index-Dsc0nyfB.js → index-BDnkSu-e.js} +1 -1
  36. package/dist/ui-dist/assets/{index-DDafGMZD.js → index-BK_XMcKr.js} +1 -1
  37. package/dist/ui-dist/assets/{index-Z_Q1PiZK.js → index-BVJz4Ljo.js} +1 -1
  38. package/dist/ui-dist/assets/{index-B44_Ufp1.js → index-BhniS9Pw.js} +1 -1
  39. package/dist/ui-dist/assets/{index-D12ZhCQn.js → index-C1GfzFGz.js} +1 -1
  40. package/dist/ui-dist/assets/{index-gBwC1Di-.js → index-CEgBGc6I.js} +1 -1
  41. package/dist/ui-dist/assets/{index-Db3ILatz.js → index-CX4G6uj_.js} +1 -1
  42. package/dist/ui-dist/assets/{index-C49iJjtK.js → index-C_LtgAgH.js} +1 -1
  43. package/dist/ui-dist/assets/{index-DKLhgppN.js → index-Cbe9HK0x.js} +1 -1
  44. package/dist/ui-dist/assets/{index-cLSOW2sv.js → index-Ch71R67s.js} +1 -1
  45. package/dist/ui-dist/assets/{index-C4H63FOl.js → index-ClxePvd_.js} +1 -1
  46. package/dist/ui-dist/assets/{index-DJ0sopR2.js → index-DYiIzKcz.js} +1 -1
  47. package/dist/ui-dist/assets/{index-JaMVMSa5.js → index-DwJ34W2C.js} +1 -1
  48. package/dist/ui-dist/assets/{index-CqPKryHo.js → index-DxHEePNM.js} +1 -1
  49. package/dist/ui-dist/assets/{index-D1fW-KBm.js → index-NoR-w-e6.js} +1 -1
  50. package/dist/ui-dist/assets/{index-DcIq-XSz.js → index-ZAoHuzda.js} +1 -1
  51. package/dist/ui-dist/assets/{index-BO_CwIEi.js → index-csURMFV9.js} +1 -1
  52. package/dist/ui-dist/assets/{index-D0kQZyqp.js → index-rSohbXog.js} +1 -1
  53. package/dist/ui-dist/assets/{index-Cr0yomNi.js → index-udadcFGS.js} +209 -209
  54. package/dist/ui-dist/assets/index-vqcwK_4v.css +1 -0
  55. package/dist/ui-dist/assets/{infoDiagram-HS3SLOUP-rWeZYbAw.js → infoDiagram-HS3SLOUP-mkdDnz14.js} +1 -1
  56. package/dist/ui-dist/assets/{journeyDiagram-XKPGCS4Q-Dyp5viM4.js → journeyDiagram-XKPGCS4Q-tRoZMyAX.js} +1 -1
  57. package/dist/ui-dist/assets/{kanban-definition-3W4ZIXB7-DlNGXRZC.js → kanban-definition-3W4ZIXB7-DF0tg_AC.js} +1 -1
  58. package/dist/ui-dist/assets/{layout-DMJwpl6V.js → layout-DOw-Nwl6.js} +1 -1
  59. package/dist/ui-dist/assets/{linear-Kfsxpfkr.js → linear-RRKJ_mN2.js} +1 -1
  60. package/dist/ui-dist/assets/{mermaid.core-BtiUU7W0.js → mermaid.core-BrQzIZWe.js} +4 -4
  61. package/dist/ui-dist/assets/{mindmap-definition-VGOIOE7T-JDTJSzUM.js → mindmap-definition-VGOIOE7T-DYP9ZkES.js} +1 -1
  62. package/dist/ui-dist/assets/{pieDiagram-ADFJNKIX-DKBlnQj4.js → pieDiagram-ADFJNKIX-ZYJvIY3k.js} +1 -1
  63. package/dist/ui-dist/assets/{quadrantDiagram-AYHSOK5B-DY-rSPWn.js → quadrantDiagram-AYHSOK5B-BRVh1Lsd.js} +1 -1
  64. package/dist/ui-dist/assets/{requirementDiagram-UZGBJVZJ-N_qgViLJ.js → requirementDiagram-UZGBJVZJ-7TlLv-to.js} +1 -1
  65. package/dist/ui-dist/assets/{sankeyDiagram-TZEHDZUN-DeaHu7Vu.js → sankeyDiagram-TZEHDZUN-CheflZmO.js} +1 -1
  66. package/dist/ui-dist/assets/{sequenceDiagram-WL72ISMW-DPdnahqv.js → sequenceDiagram-WL72ISMW-ZlJ6JLaY.js} +1 -1
  67. package/dist/ui-dist/assets/{stateDiagram-FKZM4ZOC-DZ-wCMkK.js → stateDiagram-FKZM4ZOC-bu1_QKha.js} +1 -1
  68. package/dist/ui-dist/assets/stateDiagram-v2-4FDKWEC3-Bysna4Rj.js +1 -0
  69. package/dist/ui-dist/assets/{timeline-definition-IT6M3QCI-C8-Fsp5u.js → timeline-definition-IT6M3QCI-DNx9351g.js} +1 -1
  70. package/dist/ui-dist/assets/{treemap-GDKQZRPO-IJhOKCM2.js → treemap-GDKQZRPO-ApPDTXVj.js} +1 -1
  71. package/dist/ui-dist/assets/{xychartDiagram-PRI3JC2R-DFq2CfSA.js → xychartDiagram-PRI3JC2R-C2RFLY3-.js} +1 -1
  72. package/dist/ui-dist/index.html +2 -2
  73. package/package.json +1 -1
  74. package/dist/ui-dist/assets/channel-Kt9mAvNi.js +0 -1
  75. package/dist/ui-dist/assets/classDiagram-2ON5EDUG-CrzcGMuR.js +0 -1
  76. package/dist/ui-dist/assets/classDiagram-v2-WZHVMYZB-CrzcGMuR.js +0 -1
  77. package/dist/ui-dist/assets/clone-DhtyeTi3.js +0 -1
  78. package/dist/ui-dist/assets/index-DWLL5wfa.css +0 -1
  79. package/dist/ui-dist/assets/stateDiagram-v2-4FDKWEC3-DEbMyOKZ.js +0 -1
package/dist/index.js CHANGED
@@ -3004,6 +3004,27 @@ var init_derive_account_handle_from_url = __esm({
3004
3004
  }
3005
3005
  });
3006
3006
 
3007
+ // ../packages/shared/dist/operator-timezone.js
3008
+ function resolveOperatorLocalNow(operatorTimezone) {
3009
+ const trimmed = typeof operatorTimezone === "string" ? operatorTimezone.trim() : "";
3010
+ const tz = trimmed.length > 0 ? trimmed : "UTC";
3011
+ try {
3012
+ const localNow = new Intl.DateTimeFormat("en-CA", {
3013
+ timeZone: tz,
3014
+ dateStyle: "full",
3015
+ timeStyle: "short"
3016
+ }).format(/* @__PURE__ */ new Date());
3017
+ return { tz, localNow };
3018
+ } catch {
3019
+ return { tz, localNow: (/* @__PURE__ */ new Date()).toISOString() };
3020
+ }
3021
+ }
3022
+ var init_operator_timezone = __esm({
3023
+ "../packages/shared/dist/operator-timezone.js"() {
3024
+ "use strict";
3025
+ }
3026
+ });
3027
+
3007
3028
  // ../packages/shared/dist/config-schema.js
3008
3029
  import { z as z26 } from "zod";
3009
3030
  var configMetaSchema, llmConfigSchema, databaseBackupConfigSchema, databaseConfigSchema, loggingConfigSchema, serverConfigSchema, authConfigSchema, storageLocalDiskConfigSchema, storageS3ConfigSchema, storageConfigSchema, secretsLocalEncryptedConfigSchema, secretsConfigSchema, telemetryConfigSchema, paperclipConfigSchema;
@@ -3175,6 +3196,7 @@ var init_dist = __esm({
3175
3196
  init_project_mentions();
3176
3197
  init_routine_variables();
3177
3198
  init_derive_account_handle_from_url();
3199
+ init_operator_timezone();
3178
3200
  init_config_schema();
3179
3201
  }
3180
3202
  });
@@ -20188,6 +20210,11 @@ function nonEmpty4(value) {
20188
20210
  const trimmed = value.trim();
20189
20211
  return trimmed.length > 0 ? trimmed : void 0;
20190
20212
  }
20213
+ function isTruthyEnvValue(value) {
20214
+ if (!value) return false;
20215
+ const normalized = value.trim().toLowerCase();
20216
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
20217
+ }
20191
20218
  function mergeWithDefaults(partial) {
20192
20219
  return {
20193
20220
  ...DEFAULT_BRAND_PACK,
@@ -20220,7 +20247,8 @@ function getBrandPack(env = process.env) {
20220
20247
  const hasEnvBrandOverrides = Boolean(
20221
20248
  nonEmpty4(env.PAPERCLIP_BRAND_NAME) || nonEmpty4(env.PAPERCLIP_BRAND_LOGO_URL) || nonEmpty4(env.PAPERCLIP_BRAND_HOMEPAGE_URL) || nonEmpty4(env.PAPERCLIP_BRAND_UPDATE_URL) || nonEmpty4(env.PAPERCLIP_BRAND_SUPPORT_EMAIL) || nonEmpty4(env.PAPERCLIP_BRAND_DOCS_URL)
20222
20249
  );
20223
- const loaded = hasEnvBrandOverrides ? loadBrandFromEnv(env) : DEFAULT_BRAND_PACK;
20250
+ const base = hasEnvBrandOverrides ? loadBrandFromEnv(env) : DEFAULT_BRAND_PACK;
20251
+ const loaded = isTruthyEnvValue(env.PAPERCLIP_HIDE_BRAND) ? { ...base, hideBrand: true } : base;
20224
20252
  if (env === process.env) cachedBrandPack = loaded;
20225
20253
  return loaded;
20226
20254
  }
@@ -39625,16 +39653,19 @@ function chatSessionService(db) {
39625
39653
  return rows.length > 0;
39626
39654
  },
39627
39655
  async appendMessage(companyId, sessionId, input) {
39656
+ const createdAtOverride = input.createdAt ? input.createdAt instanceof Date ? input.createdAt : new Date(input.createdAt) : null;
39628
39657
  const [row2] = await db.insert(chatMessages).values({
39629
39658
  sessionId,
39630
39659
  role: input.role,
39631
39660
  content: input.content,
39632
39661
  stopReason: input.stopReason ?? null,
39633
- usage: input.usage ?? null
39662
+ usage: input.usage ?? null,
39663
+ ...createdAtOverride ? { createdAt: createdAtOverride } : {}
39634
39664
  }).returning();
39665
+ const lastMessageAt = createdAtOverride ?? /* @__PURE__ */ new Date();
39635
39666
  const preview = extractTextPreview(input.content);
39636
39667
  await db.update(chatSessions).set({
39637
- lastMessageAt: /* @__PURE__ */ new Date(),
39668
+ lastMessageAt,
39638
39669
  lastMessagePreview: preview,
39639
39670
  updatedAt: /* @__PURE__ */ new Date()
39640
39671
  }).where(eq30(chatSessions.id, sessionId));
@@ -56529,34 +56560,34 @@ var init_agentmail2 = __esm({
56529
56560
  const inboxId = (ctx.inboxId ?? ctx.credentials.AGENT_INBOX_ID ?? "").trim();
56530
56561
  if (!apiKey) throw unprocessable("AgentMail API key is missing");
56531
56562
  if (!inboxId) throw unprocessable("AgentMail inbox id is missing");
56532
- const response = await fetch(
56533
- `https://api.agentmail.to/v0/inboxes/${encodeURIComponent(inboxId)}/messages`,
56534
- {
56535
- method: "POST",
56536
- headers: {
56537
- "Content-Type": "application/json",
56538
- Authorization: `Bearer ${apiKey}`
56539
- },
56540
- body: JSON.stringify({
56541
- from: input.from,
56542
- to: input.to,
56543
- cc: input.cc ?? [],
56544
- bcc: input.bcc ?? [],
56545
- subject: input.subject,
56546
- text: input.bodyText ?? void 0,
56547
- html: input.bodyHtml ?? void 0,
56548
- in_reply_to: input.inReplyToExternalMessageId ?? void 0,
56549
- thread_id: input.threadExternalId ?? void 0
56550
- })
56551
- }
56552
- );
56563
+ const parentMessageId = input.inReplyToExternalMessageId?.trim();
56564
+ const isReply = Boolean(parentMessageId);
56565
+ const url = isReply ? `https://api.agentmail.to/v0/inboxes/${encodeURIComponent(inboxId)}/messages/${encodeURIComponent(parentMessageId)}/reply` : `https://api.agentmail.to/v0/inboxes/${encodeURIComponent(inboxId)}/messages/send`;
56566
+ const body = {
56567
+ to: input.to,
56568
+ cc: input.cc ?? [],
56569
+ bcc: input.bcc ?? [],
56570
+ text: input.bodyText ?? void 0,
56571
+ html: input.bodyHtml ?? void 0
56572
+ };
56573
+ if (!isReply) {
56574
+ body.subject = input.subject;
56575
+ }
56576
+ const response = await fetch(url, {
56577
+ method: "POST",
56578
+ headers: {
56579
+ "Content-Type": "application/json",
56580
+ Authorization: `Bearer ${apiKey}`
56581
+ },
56582
+ body: JSON.stringify(body)
56583
+ });
56553
56584
  if (!response.ok) {
56554
56585
  throw unprocessable(`AgentMail send failed with status ${response.status}`, {
56555
56586
  body: await response.text()
56556
56587
  });
56557
56588
  }
56558
56589
  const payload = await response.json();
56559
- const externalMessageId = asString13(payload.id) ?? asString13(payload.message_id) ?? asString13(payload.messageId);
56590
+ const externalMessageId = asString13(payload.message_id) ?? asString13(payload.messageId) ?? asString13(payload.id);
56560
56591
  const externalThreadId = asString13(payload.thread_id) ?? asString13(payload.threadId) ?? input.threadExternalId;
56561
56592
  if (!externalMessageId || !externalThreadId) {
56562
56593
  throw unprocessable("AgentMail send response did not include message/thread identifiers");
@@ -56980,6 +57011,13 @@ function emailService(db) {
56980
57011
  if (filters.archived === true) whereClauses.push(isNotNull4(emailThreads2.archivedAt));
56981
57012
  if (filters.archived === false) whereClauses.push(isNull15(emailThreads2.archivedAt));
56982
57013
  if (filters.unread !== void 0) whereClauses.push(eq43(emailThreads2.unread, filters.unread));
57014
+ if (filters.direction) {
57015
+ whereClauses.push(sql31`exists (
57016
+ select 1 from email_messages em
57017
+ where em.thread_id = ${emailThreads2.id}
57018
+ and em.direction = ${filters.direction}
57019
+ )`);
57020
+ }
56983
57021
  if (filters.search?.trim()) {
56984
57022
  const q = `%${filters.search.trim()}%`;
56985
57023
  whereClauses.push(or10(ilike(emailThreads2.subject, q), sql31`exists (
@@ -57868,7 +57906,7 @@ var init_lead_product_api = __esm({
57868
57906
  });
57869
57907
 
57870
57908
  // ../server/src/services/leads.ts
57871
- import { and as and44, asc as asc18, desc as desc28, eq as eq46, gte as gte8, inArray as inArray25, sql as sql34 } from "drizzle-orm";
57909
+ import { and as and44, asc as asc18, desc as desc28, eq as eq46, gte as gte8, inArray as inArray25, or as or12, sql as sql34 } from "drizzle-orm";
57872
57910
  import crypto4 from "node:crypto";
57873
57911
  function buildLeadsListOrderBy(sortKey, sortDirRaw) {
57874
57912
  const ascDir = sortDirRaw === "asc";
@@ -57941,6 +57979,43 @@ function isRedditHostname(hostname) {
57941
57979
  const h = hostname.replace(/^www\./i, "").toLowerCase();
57942
57980
  return h === "reddit.com" || h.endsWith(".reddit.com");
57943
57981
  }
57982
+ function normalizeContactMethods(tokens) {
57983
+ if (!tokens?.length) return [];
57984
+ const set = /* @__PURE__ */ new Set();
57985
+ const allowed = SUPPORTED_PLATFORMS2;
57986
+ for (const token of tokens) {
57987
+ const key = token.trim().toLowerCase();
57988
+ if (allowed.includes(key)) set.add(key);
57989
+ }
57990
+ return Array.from(set);
57991
+ }
57992
+ function buildContactMethodsWhere(methods) {
57993
+ const parts = [];
57994
+ for (const method of methods) {
57995
+ if (method === "email") {
57996
+ parts.push(sql34`trim(coalesce(${leads.email}, '')) <> ''`);
57997
+ continue;
57998
+ }
57999
+ if (method === "reddit") {
58000
+ const primaryReddit = sql34`(${leads.primaryPlatform} = 'reddit' and trim(coalesce(${leads.primaryHandle}, '')) <> '')`;
58001
+ const noEmail = sql34`trim(coalesce(${leads.email}, '')) = ''`;
58002
+ const noPrimaryPair = sql34`not (
58003
+ trim(coalesce(${leads.primaryPlatform}, '')) <> ''
58004
+ and trim(coalesce(${leads.primaryHandle}, '')) <> ''
58005
+ )`;
58006
+ const redditProfileUrl = sql34`${leads.sourceUrl} ~* '^https?://([^/?#]*\\.)?reddit\\.com/(user|u)/[^/?#]+'`;
58007
+ parts.push(sql34`(${primaryReddit} or (${noEmail} and ${noPrimaryPair} and ${redditProfileUrl}))`);
58008
+ continue;
58009
+ }
58010
+ parts.push(
58011
+ sql34`(${leads.primaryPlatform} = ${method} and trim(coalesce(${leads.primaryHandle}, '')) <> '')`
58012
+ );
58013
+ }
58014
+ if (parts.length === 1) return parts[0];
58015
+ const merged = or12(...parts);
58016
+ if (!merged) return sql34`false`;
58017
+ return merged;
58018
+ }
57944
58019
  function parseRedditProfileUsernameFromSourceUrl(raw) {
57945
58020
  const trimmed = cleanNullable2(raw);
57946
58021
  if (!trimmed) return null;
@@ -58340,6 +58415,10 @@ function leadsService(db) {
58340
58415
  if (filters.sourceIssueId?.trim()) {
58341
58416
  whereClauses.push(eq46(leads.sourceIssueId, filters.sourceIssueId.trim()));
58342
58417
  }
58418
+ const contactMethods = normalizeContactMethods(filters.contactMethods);
58419
+ if (contactMethods.length > 0) {
58420
+ whereClauses.push(buildContactMethodsWhere(contactMethods));
58421
+ }
58343
58422
  if (filters.search?.trim()) {
58344
58423
  const like2 = `%${filters.search.trim()}%`;
58345
58424
  whereClauses.push(
@@ -58425,6 +58504,10 @@ function leadsService(db) {
58425
58504
  )`
58426
58505
  );
58427
58506
  }
58507
+ const activityContactMethods = normalizeContactMethods(options2?.contactMethods);
58508
+ if (activityContactMethods.length > 0) {
58509
+ activityWhere.push(buildContactMethodsWhere(activityContactMethods));
58510
+ }
58428
58511
  activityWhere.push(gte8(leadActivities.occurredAt, startDate));
58429
58512
  const leadWhere = [eq46(leads.companyId, companyId)];
58430
58513
  if (options2?.kind) leadWhere.push(eq46(leads.leadKind, options2.kind));
@@ -58442,6 +58525,10 @@ function leadsService(db) {
58442
58525
  )`
58443
58526
  );
58444
58527
  }
58528
+ const leadTrendContactMethods = normalizeContactMethods(options2?.contactMethods);
58529
+ if (leadTrendContactMethods.length > 0) {
58530
+ leadWhere.push(buildContactMethodsWhere(leadTrendContactMethods));
58531
+ }
58445
58532
  leadWhere.push(gte8(leads.createdAt, startDate));
58446
58533
  const [recentActivityRows, recentLeadRows] = await Promise.all([
58447
58534
  db.select({
@@ -58553,7 +58640,10 @@ function leadsService(db) {
58553
58640
  };
58554
58641
  },
58555
58642
  stats: async (companyId, filters = {}) => {
58556
- if (filters.kind) assertLeadKindShape(filters.kind);
58643
+ if (filters.kind) {
58644
+ assertLeadKindShape(filters.kind);
58645
+ await leadKindsSvc.assertKindAllowed(companyId, filters.kind);
58646
+ }
58557
58647
  if (filters.status) assertLeadStatus(filters.status);
58558
58648
  if (filters.organizationId) {
58559
58649
  await assertOrganizationInCompany(companyId, filters.organizationId);
@@ -63546,7 +63636,277 @@ var init_creators3 = __esm({
63546
63636
  }
63547
63637
  });
63548
63638
 
63639
+ // ../server/src/services/email-suggestions.ts
63640
+ import { randomUUID as randomUUID13 } from "node:crypto";
63641
+ function buildPromptBody2(ctx) {
63642
+ const lines = [];
63643
+ lines.push(`Thread: ${ctx.subject}`);
63644
+ if (ctx.participants.length > 0) {
63645
+ lines.push(`Participants: ${ctx.participants.slice(0, 6).join(", ")}`);
63646
+ }
63647
+ if (ctx.lead) {
63648
+ const parts = [];
63649
+ if (ctx.lead.displayName) parts.push(`name=${ctx.lead.displayName}`);
63650
+ if (ctx.lead.email) parts.push(`email=${ctx.lead.email}`);
63651
+ if (ctx.lead.status) parts.push(`status=${ctx.lead.status}`);
63652
+ parts.push(`everReplied=${ctx.lead.everReplied}`);
63653
+ parts.push(`everSignedUp=${ctx.lead.everSignedUp}`);
63654
+ if (ctx.lead.lastContactedAt) parts.push(`lastContactedAt=${ctx.lead.lastContactedAt}`);
63655
+ if (ctx.lead.lastReplyAt) parts.push(`lastReplyAt=${ctx.lead.lastReplyAt}`);
63656
+ lines.push(`Lead: ${parts.join(" ")}`);
63657
+ if (ctx.lead.hungrySignal) lines.push(`HungrySignal: ${ctx.lead.hungrySignal.slice(0, 300)}`);
63658
+ if (ctx.lead.notes) lines.push(`Notes: ${ctx.lead.notes.slice(0, 500)}`);
63659
+ } else {
63660
+ lines.push("Lead: (none linked)");
63661
+ }
63662
+ if (ctx.lastMessages.length > 0) {
63663
+ lines.push("");
63664
+ lines.push("Last messages (oldest first):");
63665
+ for (const m of ctx.lastMessages) {
63666
+ lines.push(`- [${m.sentAt}] ${m.direction}: ${m.snippet}`);
63667
+ }
63668
+ }
63669
+ if (ctx.recentActivities.length > 0) {
63670
+ lines.push("");
63671
+ lines.push("Recent activity:");
63672
+ for (const a of ctx.recentActivities) {
63673
+ lines.push(`- ${a.occurredAt} ${a.kind}`);
63674
+ }
63675
+ }
63676
+ lines.push("");
63677
+ lines.push(`Kinds and params:
63678
+ - do_nothing: {} (use when no action is warranted yet)
63679
+ - send_follow_up: { reopen?: boolean } (MUST include draftBody with the suggested reply text)
63680
+ - archive_thread: {} (use when the thread is dead or resolved)
63681
+ - mark_lead_status: { status: drafted|contacted|replied|signed_up|dead }
63682
+ - create_handoff_issue: { title: string, description?: string, priority?: urgent|high|medium|low }
63683
+ Use when a human needs to do a manual action elsewhere (post a DM, call them, paste into a tool).`);
63684
+ lines.push("");
63685
+ lines.push(
63686
+ 'Return JSON only: {"reasoning":"why these","actions":[{"kind":"...","label":"...","description":"...","params":{...},"draftBody":"..."}]}'
63687
+ );
63688
+ return lines.join("\n");
63689
+ }
63690
+ function snippetOf(message) {
63691
+ const text87 = message.bodyText ?? message.bodyHtml ?? "";
63692
+ const cleaned = text87.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
63693
+ return cleaned.slice(0, 240);
63694
+ }
63695
+ function validateActions2(raw) {
63696
+ if (!raw || typeof raw !== "object") return [];
63697
+ const arr = raw.actions;
63698
+ if (!Array.isArray(arr)) return [];
63699
+ const out = [];
63700
+ for (const entry of arr) {
63701
+ if (!entry || typeof entry !== "object") continue;
63702
+ const e = entry;
63703
+ const kind = e.kind;
63704
+ if (typeof kind !== "string" || !VALID_KINDS2.has(kind)) continue;
63705
+ const label = typeof e.label === "string" ? e.label.trim() : "";
63706
+ const description = typeof e.description === "string" ? e.description.trim() : "";
63707
+ if (!label || !description) continue;
63708
+ const params = e.params && typeof e.params === "object" ? e.params : {};
63709
+ const draftBody = typeof e.draftBody === "string" ? e.draftBody : void 0;
63710
+ const kindTyped = kind;
63711
+ let validated = null;
63712
+ switch (kindTyped) {
63713
+ case "do_nothing":
63714
+ case "archive_thread": {
63715
+ validated = {
63716
+ kind: kindTyped,
63717
+ label: label.slice(0, 40),
63718
+ description: description.slice(0, 120),
63719
+ params: {}
63720
+ };
63721
+ break;
63722
+ }
63723
+ case "send_follow_up": {
63724
+ if (!draftBody || draftBody.trim().length === 0) break;
63725
+ validated = {
63726
+ kind: kindTyped,
63727
+ label: label.slice(0, 40),
63728
+ description: description.slice(0, 120),
63729
+ params: {},
63730
+ draftBody: draftBody.replace(/—/g, "-").replace(/–/g, "-")
63731
+ };
63732
+ break;
63733
+ }
63734
+ case "mark_lead_status": {
63735
+ const status = typeof params.status === "string" ? params.status : null;
63736
+ if (!status || !VALID_LEAD_STATUSES.has(status)) break;
63737
+ validated = {
63738
+ kind: kindTyped,
63739
+ label: label.slice(0, 40),
63740
+ description: description.slice(0, 120),
63741
+ params: { status }
63742
+ };
63743
+ break;
63744
+ }
63745
+ case "create_handoff_issue": {
63746
+ const title = typeof params.title === "string" ? params.title.trim() : "";
63747
+ if (!title) break;
63748
+ const rawDesc = typeof params.description === "string" ? params.description.trim() : "";
63749
+ const descParam = rawDesc ? rawDesc.replace(/—/g, "-").slice(0, 1e3) : void 0;
63750
+ const priorityRaw = typeof params.priority === "string" ? params.priority : void 0;
63751
+ const priority = priorityRaw && VALID_PRIORITIES2.has(priorityRaw) ? priorityRaw : void 0;
63752
+ validated = {
63753
+ kind: kindTyped,
63754
+ label: label.slice(0, 40),
63755
+ description: description.slice(0, 120),
63756
+ params: {
63757
+ title: title.slice(0, 200),
63758
+ ...descParam ? { description: descParam } : {},
63759
+ ...priority ? { priority } : {}
63760
+ }
63761
+ };
63762
+ break;
63763
+ }
63764
+ }
63765
+ if (validated) {
63766
+ let cleanLabel = validated.label.replace(/—/g, "-").replace(/–/g, "-").trim();
63767
+ if (cleanLabel.length > 0) {
63768
+ cleanLabel = cleanLabel[0].toUpperCase() + cleanLabel.slice(1);
63769
+ }
63770
+ validated.label = cleanLabel;
63771
+ let cleanDescription = validated.description.replace(/—/g, "-").replace(/–/g, "-").trim();
63772
+ if (cleanDescription.length > 0) {
63773
+ cleanDescription = cleanDescription[0].toUpperCase() + cleanDescription.slice(1);
63774
+ }
63775
+ validated.description = cleanDescription;
63776
+ out.push(validated);
63777
+ if (out.length >= 3) break;
63778
+ }
63779
+ }
63780
+ return out;
63781
+ }
63782
+ function emailSuggestionsService(db) {
63783
+ const emails = emailService(db);
63784
+ const leadsSvc = leadsService(db);
63785
+ async function buildContext2(companyId, threadId) {
63786
+ const detail = await emails.getThreadDetail(companyId, threadId);
63787
+ const messages = detail.messages ?? [];
63788
+ const lastMessages = messages.slice(-3).map((m) => ({
63789
+ direction: m.direction,
63790
+ sentAt: m.sentAt instanceof Date ? m.sentAt.toISOString() : String(m.sentAt),
63791
+ snippet: snippetOf(m)
63792
+ }));
63793
+ let lead = null;
63794
+ let recentActivities = [];
63795
+ if (detail.leadId) {
63796
+ try {
63797
+ const leadDetail = await leadsSvc.get(companyId, detail.leadId);
63798
+ const isoOrNull = (v) => {
63799
+ if (!v) return null;
63800
+ if (v instanceof Date) return v.toISOString();
63801
+ if (typeof v === "string") return v;
63802
+ return null;
63803
+ };
63804
+ lead = {
63805
+ displayName: leadDetail.displayName ?? null,
63806
+ email: leadDetail.email ?? null,
63807
+ status: leadDetail.status ?? null,
63808
+ lastContactedAt: isoOrNull(leadDetail.lastContactedAt),
63809
+ lastReplyAt: isoOrNull(leadDetail.lastReplyAt),
63810
+ everReplied: Boolean(leadDetail.everReplied),
63811
+ everSignedUp: Boolean(leadDetail.everSignedUp),
63812
+ hungrySignal: leadDetail.hungrySignal ?? null,
63813
+ notes: leadDetail.notes ?? null
63814
+ };
63815
+ recentActivities = (leadDetail.activities ?? []).slice(0, 5).map((a) => ({
63816
+ kind: a.kind,
63817
+ occurredAt: isoOrNull(a.occurredAt) ?? ""
63818
+ }));
63819
+ } catch {
63820
+ }
63821
+ }
63822
+ return {
63823
+ threadId: detail.id,
63824
+ subject: detail.subject ?? "(no subject)",
63825
+ participants: (detail.participants ?? []).map(
63826
+ (p17) => typeof p17 === "string" ? p17 : p17.email ?? ""
63827
+ ).filter((s) => s.length > 0),
63828
+ lastMessages,
63829
+ lead,
63830
+ recentActivities
63831
+ };
63832
+ }
63833
+ async function generate(companyId, threadId) {
63834
+ const ctx = await buildContext2(companyId, threadId);
63835
+ const prompt = `${SYSTEM_PROMPT2}
63836
+
63837
+ ${buildPromptBody2(ctx)}`;
63838
+ const cliOutput = await runClaudeOneShot(prompt, {
63839
+ model: EMAIL_SUGGESTION_MODEL,
63840
+ timeoutMs: EMAIL_SUGGESTION_TIMEOUT_MS
63841
+ });
63842
+ const text87 = extractAssistantText(cliOutput);
63843
+ const raw = extractJsonObject(text87);
63844
+ const actions = validateActions2(raw);
63845
+ const rawArr = raw?.actions;
63846
+ const rawCount = Array.isArray(rawArr) ? rawArr.length : 0;
63847
+ console.log(
63848
+ "[email-suggestions] thread=%s rawCount=%d validCount=%d raw=%s",
63849
+ threadId,
63850
+ rawCount,
63851
+ actions.length,
63852
+ JSON.stringify(raw).slice(0, 1500)
63853
+ );
63854
+ const rawReasoning = raw?.reasoning;
63855
+ const reasoning = typeof rawReasoning === "string" && rawReasoning.trim().length > 0 ? rawReasoning.trim().replace(/—/g, "-").replace(/–/g, "-").slice(0, 400) : null;
63856
+ if (actions.length === 0) {
63857
+ throw new SuggestionsUnavailableError("no_valid_actions");
63858
+ }
63859
+ return {
63860
+ actions,
63861
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
63862
+ eventId: randomUUID13(),
63863
+ reasoning
63864
+ };
63865
+ }
63866
+ return { generate, buildContext: buildContext2 };
63867
+ }
63868
+ var EMAIL_SUGGESTION_MODEL, EMAIL_SUGGESTION_TIMEOUT_MS, VALID_KINDS2, VALID_LEAD_STATUSES, VALID_PRIORITIES2, SYSTEM_PROMPT2;
63869
+ var init_email_suggestions = __esm({
63870
+ "../server/src/services/email-suggestions.ts"() {
63871
+ "use strict";
63872
+ init_email_service();
63873
+ init_leads2();
63874
+ init_issue_suggestions();
63875
+ EMAIL_SUGGESTION_MODEL = process.env.PAPERCLIP_EMAIL_SUGGESTION_MODEL ?? process.env.PAPERCLIP_SUGGESTION_MODEL ?? "claude-haiku-4-5";
63876
+ EMAIL_SUGGESTION_TIMEOUT_MS = Number(
63877
+ process.env.PAPERCLIP_EMAIL_SUGGESTION_TIMEOUT_MS ?? "60000"
63878
+ );
63879
+ VALID_KINDS2 = /* @__PURE__ */ new Set([
63880
+ "do_nothing",
63881
+ "send_follow_up",
63882
+ "archive_thread",
63883
+ "mark_lead_status",
63884
+ "create_handoff_issue"
63885
+ ]);
63886
+ VALID_LEAD_STATUSES = /* @__PURE__ */ new Set([
63887
+ "drafted",
63888
+ "contacted",
63889
+ "replied",
63890
+ "signed_up",
63891
+ "dead"
63892
+ ]);
63893
+ VALID_PRIORITIES2 = /* @__PURE__ */ new Set(["urgent", "high", "medium", "low"]);
63894
+ SYSTEM_PROMPT2 = `Suggest 1-3 next actions for a human operator viewing an email thread in Paperclip.
63895
+ Be decisive and concrete. Pick the most useful single action when one is obvious, never pad.
63896
+ Heuristics:
63897
+ - If we contacted them less than 3 days ago and got no reply, "do_nothing" is usually correct.
63898
+ - If they replied recently, suggest "send_follow_up" with a tight draftBody.
63899
+ - If it has been more than 7 days since our last touch with no reply, suggest "send_follow_up".
63900
+ - If they explicitly declined, suggest "mark_lead_status" with status=dead.
63901
+ - If a human needs to take an external action (not just email), use "create_handoff_issue".
63902
+
63903
+ Style: no em dashes (use hyphens or commas). Labels under 40 chars. Descriptions under 120 chars.
63904
+ Respond with JSON ONLY, no prose before or after: {"reasoning":"...","actions":[...]}`;
63905
+ }
63906
+ });
63907
+
63549
63908
  // ../server/src/routes/emails.ts
63909
+ import { randomUUID as randomUUID14 } from "node:crypto";
63550
63910
  import { Router as Router14 } from "express";
63551
63911
  import { z as z31 } from "zod";
63552
63912
  function normalizeHeaders(headers) {
@@ -63563,6 +63923,7 @@ function normalizeHeaders(headers) {
63563
63923
  function emailRoutes(db) {
63564
63924
  const router = Router14();
63565
63925
  const svc = emailService(db);
63926
+ const suggestionsSvc = emailSuggestionsService(db);
63566
63927
  router.get("/companies/:companyId/email-threads", async (req, res) => {
63567
63928
  const companyId = req.params.companyId;
63568
63929
  assertCompanyAccess(req, companyId);
@@ -63645,6 +64006,60 @@ function emailRoutes(db) {
63645
64006
  });
63646
64007
  res.status(201).json(thread);
63647
64008
  });
64009
+ router.post(
64010
+ "/companies/:companyId/email-threads/:threadId/suggested-actions",
64011
+ async (req, res) => {
64012
+ const { companyId, threadId } = req.params;
64013
+ assertCompanyAccess(req, companyId);
64014
+ try {
64015
+ const result = await suggestionsSvc.generate(companyId, threadId);
64016
+ const actor = getActorInfo(req);
64017
+ await logActivity(db, {
64018
+ companyId,
64019
+ actorType: actor.actorType,
64020
+ actorId: actor.actorId,
64021
+ agentId: actor.agentId,
64022
+ runId: actor.runId,
64023
+ action: "email.thread.suggestions_generated",
64024
+ entityType: "email_thread",
64025
+ entityId: threadId,
64026
+ details: {
64027
+ eventId: result.eventId,
64028
+ actionCount: result.actions.length,
64029
+ actions: result.actions.map((a) => ({
64030
+ kind: a.kind,
64031
+ label: a.label,
64032
+ description: a.description,
64033
+ params: a.params
64034
+ })),
64035
+ generatedAt: result.generatedAt
64036
+ }
64037
+ });
64038
+ res.json({
64039
+ actions: result.actions,
64040
+ generatedAt: result.generatedAt,
64041
+ eventId: result.eventId,
64042
+ ...result.reasoning ? { reasoning: result.reasoning } : {}
64043
+ });
64044
+ } catch (err) {
64045
+ if (err instanceof SuggestionsUnavailableError) {
64046
+ const correlationId = randomUUID14();
64047
+ logger.warn(
64048
+ { threadId, reason: err.reason, message: err.message, correlationId },
64049
+ "email_suggestions_unavailable"
64050
+ );
64051
+ res.status(503).json({
64052
+ error: "suggestions_unavailable",
64053
+ reason: err.reason,
64054
+ message: err.message,
64055
+ correlationId
64056
+ });
64057
+ return;
64058
+ }
64059
+ throw err;
64060
+ }
64061
+ }
64062
+ );
63648
64063
  router.post("/webhooks/email/:provider", async (req, res) => {
63649
64064
  const providerId = String(req.params.provider ?? "").trim();
63650
64065
  const rawBody = req.rawBody;
@@ -63672,12 +64087,16 @@ var init_emails2 = __esm({
63672
64087
  init_errors();
63673
64088
  init_activity_log2();
63674
64089
  init_email_service();
64090
+ init_email_suggestions();
64091
+ init_issue_suggestions();
64092
+ init_logger();
63675
64093
  init_authz();
63676
64094
  listThreadsQuerySchema = z31.object({
63677
64095
  agentId: z31.string().uuid().optional(),
63678
64096
  leadId: z31.string().uuid().optional(),
63679
64097
  archived: z31.enum(["true", "false"]).optional().transform((value) => value === void 0 ? void 0 : value === "true"),
63680
64098
  unread: z31.enum(["true", "false"]).optional().transform((value) => value === void 0 ? void 0 : value === "true"),
64099
+ direction: z31.enum(["inbound", "outbound"]).optional(),
63681
64100
  search: z31.string().trim().min(1).optional(),
63682
64101
  cursor: z31.string().trim().min(1).optional(),
63683
64102
  limit: z31.coerce.number().int().min(1).max(200).optional()
@@ -63719,6 +64138,7 @@ function leadRoutes(db) {
63719
64138
  const companyId = req.params.companyId;
63720
64139
  assertCompanyAccess(req, companyId);
63721
64140
  const labelIds = String(req.query.labelIds ?? "").split(",").map((value) => value.trim()).filter(Boolean);
64141
+ const contactMethods = String(req.query.contactMethods ?? "").split(",").map((value) => value.trim()).filter(Boolean);
63722
64142
  const includeTotal = String(req.query.includeTotal ?? "") === "true";
63723
64143
  const sortKey = req.query.sortKey?.trim() || void 0;
63724
64144
  const sortDirRaw = req.query.sortDir;
@@ -63728,6 +64148,7 @@ function leadRoutes(db) {
63728
64148
  status: req.query.status || void 0,
63729
64149
  search: req.query.q || void 0,
63730
64150
  labelIds,
64151
+ contactMethods,
63731
64152
  organizationId: req.query.organizationId || void 0,
63732
64153
  hideDead: String(req.query.hideDead ?? "") === "true",
63733
64154
  platform: req.query.platform || void 0,
@@ -63763,12 +64184,14 @@ function leadRoutes(db) {
63763
64184
  assertCompanyAccess(req, companyId);
63764
64185
  const daysParam = Number(req.query.days ?? 30);
63765
64186
  const labelIds = String(req.query.labelIds ?? "").split(",").map((value) => value.trim()).filter(Boolean);
64187
+ const contactMethods = String(req.query.contactMethods ?? "").split(",").map((value) => value.trim()).filter(Boolean);
63766
64188
  const stats = await svc.activityStats(companyId, {
63767
64189
  days: Number.isFinite(daysParam) ? daysParam : 30,
63768
64190
  kind: req.query.kind || void 0,
63769
64191
  status: req.query.status || void 0,
63770
64192
  search: req.query.q || void 0,
63771
64193
  labelIds,
64194
+ contactMethods,
63772
64195
  hideDead: String(req.query.hideDead ?? "") === "true"
63773
64196
  });
63774
64197
  res.json(stats);
@@ -63920,7 +64343,7 @@ var init_lead_kinds3 = __esm({
63920
64343
  });
63921
64344
 
63922
64345
  // ../server/src/services/content.ts
63923
- import { and as and47, desc as desc29, eq as eq50, gte as gte9, inArray as inArray26, lte as lte6, not as not3, or as or12, sql as sql35 } from "drizzle-orm";
64346
+ import { and as and47, desc as desc29, eq as eq50, gte as gte9, inArray as inArray26, lte as lte6, not as not3, or as or13, sql as sql35 } from "drizzle-orm";
63924
64347
  import { execFile as execFile13 } from "node:child_process";
63925
64348
  function assertKind(value) {
63926
64349
  if (!SUPPORTED_KINDS.includes(value)) {
@@ -63956,9 +64379,9 @@ function contentListMediaTypeWhere(mediaType) {
63956
64379
  select 1 from jsonb_array_elements(${contents.media}) as elem
63957
64380
  where coalesce(elem->>'mimeType', '') ilike 'audio/%'
63958
64381
  )`;
63959
- const imageSignal = or12(inArray26(contents.kind, [...CONTENT_IMAGE_KINDS]), imageMime);
63960
- const videoSignal = or12(inArray26(contents.kind, [...CONTENT_VIDEO_KINDS]), videoMime);
63961
- const audioSignal = or12(inArray26(contents.kind, [...CONTENT_AUDIO_KINDS]), audioMime);
64382
+ const imageSignal = or13(inArray26(contents.kind, [...CONTENT_IMAGE_KINDS]), imageMime);
64383
+ const videoSignal = or13(inArray26(contents.kind, [...CONTENT_VIDEO_KINDS]), videoMime);
64384
+ const audioSignal = or13(inArray26(contents.kind, [...CONTENT_AUDIO_KINDS]), audioMime);
63962
64385
  switch (mediaType) {
63963
64386
  case "carousel":
63964
64387
  return isCarouselKind;
@@ -66133,7 +66556,7 @@ var init_content2 = __esm({
66133
66556
  });
66134
66557
 
66135
66558
  // ../server/src/services/calendar.ts
66136
- import { and as and50, asc as asc20, eq as eq53, gte as gte10, inArray as inArray28, lte as lte7, or as or13, sql as sql36 } from "drizzle-orm";
66559
+ import { and as and50, asc as asc20, eq as eq53, gte as gte10, inArray as inArray28, lte as lte7, or as or14, sql as sql36 } from "drizzle-orm";
66137
66560
  function cleanNullable4(value) {
66138
66561
  if (value == null) return null;
66139
66562
  const trimmed = value.trim();
@@ -66219,7 +66642,7 @@ function calendarService(db) {
66219
66642
  ];
66220
66643
  if (options2.assignees && options2.assignees.length > 0) {
66221
66644
  where.push(
66222
- or13(
66645
+ or14(
66223
66646
  inArray28(issues.assigneeAgentId, options2.assignees),
66224
66647
  inArray28(issues.assigneeUserId, options2.assignees)
66225
66648
  )
@@ -66260,7 +66683,7 @@ function calendarService(db) {
66260
66683
  async function fetchContentItems(companyId, from, to, options2, limit) {
66261
66684
  const where = [
66262
66685
  eq53(contents.companyId, companyId),
66263
- or13(
66686
+ or14(
66264
66687
  and50(
66265
66688
  sql36`${contents.scheduledAt} is not null`,
66266
66689
  gte10(contents.scheduledAt, from),
@@ -66281,7 +66704,7 @@ function calendarService(db) {
66281
66704
  }
66282
66705
  if (options2.assignees && options2.assignees.length > 0) {
66283
66706
  where.push(
66284
- or13(
66707
+ or14(
66285
66708
  inArray28(contents.assignedAgentId, options2.assignees),
66286
66709
  inArray28(contents.assignedUserId, options2.assignees)
66287
66710
  )
@@ -68089,7 +68512,7 @@ import { readFile as readFile4 } from "node:fs/promises";
68089
68512
  import path59 from "node:path";
68090
68513
  import { fileURLToPath as fileURLToPath16 } from "node:url";
68091
68514
  import { and as and52, desc as desc32, eq as eq55, gte as gte11, inArray as inArray30, lte as lte8, ne as ne10, sql as sql38 } from "drizzle-orm";
68092
- import { randomUUID as randomUUID13 } from "node:crypto";
68515
+ import { randomUUID as randomUUID15 } from "node:crypto";
68093
68516
  function readNonEmptyConfigString2(value) {
68094
68517
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
68095
68518
  }
@@ -68799,7 +69222,14 @@ function chatTriageService(db, storage) {
68799
69222
  const validAgentIds = new Set(ctx.agents.map((a) => a.id));
68800
69223
  const agentNameById = new Map(ctx.agents.map((a) => [a.id, a.name]));
68801
69224
  const parts = [];
68802
- parts.push(SYSTEM_PROMPT2);
69225
+ parts.push(SYSTEM_PROMPT3);
69226
+ {
69227
+ const { tz, localNow } = resolveOperatorLocalNow(options2?.operatorTimezone);
69228
+ parts.push("");
69229
+ parts.push(
69230
+ `Operator local timezone: ${tz}. Current local time: ${localNow}. When you state any time ("posted at", "scheduled for", "today", "yesterday", "X minutes ago"), convert from UTC ISO timestamps to this timezone before speaking. Never quote a raw UTC time to the operator.`
69231
+ );
69232
+ }
68803
69233
  if (mode === "live") {
68804
69234
  parts.push("");
68805
69235
  parts.push(LIVE_MODE_SYSTEM_ADDENDUM);
@@ -68865,7 +69295,7 @@ ${message}`);
68865
69295
  parts.push("Respond with JSON only.");
68866
69296
  if (toolUseEnabled) {
68867
69297
  const targetAgentId = options2?.targetAgentId ?? null;
68868
- const jwt = targetAgentId ? createLocalAgentJwt(targetAgentId, companyId, "claude_local", randomUUID13()) : null;
69298
+ const jwt = targetAgentId ? createLocalAgentJwt(targetAgentId, companyId, "claude_local", randomUUID15()) : null;
68869
69299
  if (targetAgentId && !jwt) {
68870
69300
  toolUseFallbackReason = "agent_auth_unavailable";
68871
69301
  toolUseFallbackAdapterType = adapterType;
@@ -68920,7 +69350,7 @@ ${message}`);
68920
69350
  const entries = parseClaudeStdoutLine(line, "");
68921
69351
  for (const entry of entries) {
68922
69352
  if (entry.kind === "tool_call") {
68923
- const id = entry.toolUseId ?? `${entry.name}:${randomUUID13()}`;
69353
+ const id = entry.toolUseId ?? `${entry.name}:${randomUUID15()}`;
68924
69354
  const input = entry.input && typeof entry.input === "object" && !Array.isArray(entry.input) ? entry.input : {};
68925
69355
  onToolEvent({ kind: "tool_call", id, name: entry.name, input });
68926
69356
  } else if (entry.kind === "tool_result" && entry.toolUseId) {
@@ -69009,12 +69439,13 @@ ${message}`);
69009
69439
  }
69010
69440
  return { triage };
69011
69441
  }
69012
- var CHAT_STYLE_MAX_BYTES, SKILL_PRIMER_MAX_BYTES, RECENT_COMMENT_LIMIT, COMMENT_BODY_PREVIEW_CHARS, CHAT_TRIAGE_MODEL, CHAT_TRIAGE_TIMEOUT_MS, PRIORITY_VALUES, GOAL_LEVEL_VALUES, LEAD_KIND_VALUES, LEAD_PLATFORM_VALUES, MAX_PROPOSALS_TOTAL, CONTEXT_CACHE_TTL_MS, CONTEXT_CACHE_MAX_SIZE, contextCache, SYSTEM_PROMPT2, CHAT_PERSONA_MAX_CHARS, PERSONA_MODE_REPLY_HINT, MAX_ENV_KEYS_PER_AGENT, cachedChatStyleOverride, cachedSkillPrimer, VISION_IMAGE_MIME_TYPES, PDF_MIME_TYPE, ATTACHMENT_TOTAL_BYTE_CAP, LIVE_MODE_SYSTEM_ADDENDUM, VOICE_CALL_REPLY_ADDENDUM, POST_CALL_MODE_SYSTEM_ADDENDUM;
69442
+ var CHAT_STYLE_MAX_BYTES, SKILL_PRIMER_MAX_BYTES, RECENT_COMMENT_LIMIT, COMMENT_BODY_PREVIEW_CHARS, CHAT_TRIAGE_MODEL, CHAT_TRIAGE_TIMEOUT_MS, PRIORITY_VALUES, GOAL_LEVEL_VALUES, LEAD_KIND_VALUES, LEAD_PLATFORM_VALUES, MAX_PROPOSALS_TOTAL, CONTEXT_CACHE_TTL_MS, CONTEXT_CACHE_MAX_SIZE, contextCache, SYSTEM_PROMPT3, CHAT_PERSONA_MAX_CHARS, PERSONA_MODE_REPLY_HINT, MAX_ENV_KEYS_PER_AGENT, cachedChatStyleOverride, cachedSkillPrimer, VISION_IMAGE_MIME_TYPES, PDF_MIME_TYPE, ATTACHMENT_TOTAL_BYTE_CAP, LIVE_MODE_SYSTEM_ADDENDUM, VOICE_CALL_REPLY_ADDENDUM, POST_CALL_MODE_SYSTEM_ADDENDUM;
69013
69443
  var init_chat_triage = __esm({
69014
69444
  "../server/src/services/chat-triage.ts"() {
69015
69445
  "use strict";
69016
69446
  init_dist2();
69017
69447
  init_issue_suggestions();
69448
+ init_dist();
69018
69449
  init_agent_instructions();
69019
69450
  init_dashboard();
69020
69451
  init_acquisition();
@@ -69056,7 +69487,7 @@ var init_chat_triage = __esm({
69056
69487
  CONTEXT_CACHE_TTL_MS = 6e4;
69057
69488
  CONTEXT_CACHE_MAX_SIZE = 32;
69058
69489
  contextCache = /* @__PURE__ */ new Map();
69059
- SYSTEM_PROMPT2 = `You are an assistant embedded inside Paperclip, the operator's control plane for a company of AI agents. The operator is talking to you to think out loud, ask questions about their company, and occasionally capture work.
69490
+ SYSTEM_PROMPT3 = `You are an assistant embedded inside Paperclip, the operator's control plane for a company of AI agents. The operator is talking to you to think out loud, ask questions about their company, and occasionally capture work.
69060
69491
 
69061
69492
  Your primary job is to be a good conversational partner. Most turns you will just reply with text - answer questions, ask clarifying follow-ups, explore ideas together. Tables in the reply still count as conversational output when they are the clearest shape. Only when something sounds like a concrete artifact the operator wants captured should you propose creating one.
69062
69493
 
@@ -69340,12 +69771,13 @@ function chatRoutes(db, storageService, bridgeDeps) {
69340
69771
  mode: parsed.data.mode,
69341
69772
  previousProposalFingerprints: parsed.data.previousProposalFingerprints,
69342
69773
  voiceCallActive,
69774
+ operatorTimezone: parsed.data.operatorTimezone,
69343
69775
  onToolEvent: wantsStream ? (event) => writeNdjson({ type: "tool_event", event }) : void 0
69344
69776
  }
69345
69777
  );
69346
69778
  let continuationRunId = null;
69347
69779
  let continuationScheduled = false;
69348
- if (result.continuation && parsed.data.sessionId && parsed.data.userMessageId) {
69780
+ if (result.continuation && parsed.data.sessionId && parsed.data.userMessageId && !voiceCallActive) {
69349
69781
  const continuationSession = await sessionSvc.getSession(companyId, parsed.data.sessionId);
69350
69782
  const preferredAgentId = result.continuation.preferredAgentId ?? continuationSession?.targetAgentId ?? await db.select({ id: agents.id }).from(agents).where(
69351
69783
  and53(
@@ -69795,7 +70227,8 @@ var init_chat2 = __esm({
69795
70227
  userMessageId: z34.string().uuid().optional(),
69796
70228
  attachments: z34.array(triageAttachmentSchema).max(8).optional(),
69797
70229
  mode: z34.enum(["full", "live"]).optional(),
69798
- previousProposalFingerprints: z34.array(z34.string().min(1).max(200)).max(50).optional()
70230
+ previousProposalFingerprints: z34.array(z34.string().min(1).max(200)).max(50).optional(),
70231
+ operatorTimezone: z34.string().min(1).max(100).optional()
69799
70232
  });
69800
70233
  blockMetadataSchema = z34.record(z34.string(), z34.unknown()).optional();
69801
70234
  blockSchema = z34.discriminatedUnion("type", [
@@ -75681,7 +76114,7 @@ var init_plugin_lifecycle = __esm({
75681
76114
  });
75682
76115
 
75683
76116
  // ../server/src/services/voice-intent-dispatcher.ts
75684
- import { randomUUID as randomUUID14 } from "node:crypto";
76117
+ import { randomUUID as randomUUID16 } from "node:crypto";
75685
76118
  import { and as and58, eq as eq61 } from "drizzle-orm";
75686
76119
  function gcPendingResults(now) {
75687
76120
  for (const [key, value] of pendingResults) {
@@ -75705,19 +76138,20 @@ async function loadAgentAdapterConfig(db, companyId, agentId) {
75705
76138
  return row2.adapterConfig;
75706
76139
  }
75707
76140
  async function dispatchVoiceIntent(deps, input) {
75708
- const { db, onComplete } = deps;
76141
+ const { db, onComplete, onToolEvent } = deps;
75709
76142
  const intent = readNonEmpty(input.intent);
75710
76143
  const companyId = readNonEmpty(input.companyId);
75711
76144
  const agentId = readNonEmpty(input.agentId);
76145
+ const operatorTimezone = readNonEmpty(input.operatorTimezone);
75712
76146
  const chatSessionId = readNonEmpty(input.chatSessionId);
75713
76147
  const surface = input.surface ?? "voice";
75714
76148
  const correlationId = readNonEmpty(input.correlationId) ?? null;
75715
76149
  const sessions2 = chatSessionService(db);
75716
- const append = async (text87) => {
76150
+ const append = async (text87, role = "system") => {
75717
76151
  if (!chatSessionId || !companyId) return;
75718
76152
  try {
75719
76153
  await sessions2.appendMessage(companyId, chatSessionId, {
75720
- role: "system",
76154
+ role,
75721
76155
  content: [
75722
76156
  {
75723
76157
  type: "text",
@@ -75754,7 +76188,7 @@ async function dispatchVoiceIntent(deps, input) {
75754
76188
  finish(text87, false);
75755
76189
  return;
75756
76190
  }
75757
- const jwt = createLocalAgentJwt(agentId, companyId, "claude_local", randomUUID14());
76191
+ const jwt = createLocalAgentJwt(agentId, companyId, "claude_local", randomUUID16());
75758
76192
  if (!jwt) {
75759
76193
  const text87 = `[${getBrandLogTag()}: agent auth unavailable, try again after the call]`;
75760
76194
  await append(text87);
@@ -75772,13 +76206,52 @@ async function dispatchVoiceIntent(deps, input) {
75772
76206
  finish(text87, false);
75773
76207
  return;
75774
76208
  }
76209
+ const { tz, localNow } = resolveOperatorLocalNow(operatorTimezone);
75775
76210
  const prompt = [
75776
- SYSTEM_PROMPT3,
76211
+ SYSTEM_PROMPT4,
76212
+ "",
76213
+ `Operator local timezone: ${tz}. Current local time: ${localNow}. Convert every UTC ISO timestamp you encounter to this timezone before speaking it.`,
75777
76214
  "",
75778
76215
  `Operator intent: ${intent}`,
75779
76216
  "",
75780
76217
  "Reply with the final plain-text answer only."
75781
76218
  ].join("\n");
76219
+ const onStdoutLine = onToolEvent ? (line) => {
76220
+ const entries = parseClaudeStdoutLine(line, "");
76221
+ for (const entry of entries) {
76222
+ if (entry.kind === "tool_call") {
76223
+ const id = entry.toolUseId ?? `${entry.name}:${randomUUID16()}`;
76224
+ const inputObj = entry.input && typeof entry.input === "object" && !Array.isArray(entry.input) ? entry.input : {};
76225
+ try {
76226
+ onToolEvent({
76227
+ kind: "tool_call",
76228
+ companyId,
76229
+ agentId,
76230
+ chatSessionId,
76231
+ correlationId,
76232
+ id,
76233
+ name: entry.name,
76234
+ input: inputObj
76235
+ });
76236
+ } catch {
76237
+ }
76238
+ } else if (entry.kind === "tool_result" && entry.toolUseId) {
76239
+ try {
76240
+ onToolEvent({
76241
+ kind: "tool_result",
76242
+ companyId,
76243
+ agentId,
76244
+ chatSessionId,
76245
+ correlationId,
76246
+ id: entry.toolUseId,
76247
+ content: entry.content,
76248
+ isError: entry.isError
76249
+ });
76250
+ } catch {
76251
+ }
76252
+ }
76253
+ }
76254
+ } : void 0;
75782
76255
  try {
75783
76256
  const cliOutput = await runClaudeOneShot(prompt, {
75784
76257
  model: DISPATCH_MODEL,
@@ -75793,13 +76266,18 @@ async function dispatchVoiceIntent(deps, input) {
75793
76266
  PAPERCLIP_API_URL: process.env.PAPERCLIP_API_URL ?? "http://127.0.0.1:3100",
75794
76267
  PAPERCLIP_COMPANY_ID: companyId,
75795
76268
  PAPERCLIP_AGENT_ID: agentId
75796
- }
76269
+ },
76270
+ onStdoutLine
75797
76271
  });
75798
76272
  const finalText = extractFinalText(cliOutput).trim();
75799
- const tag = getBrandLogTag();
75800
- const text87 = finalText.length > 0 ? `[${tag}] ${finalText}` : `[${tag}: no response]`;
75801
- await append(text87);
75802
- finish(text87, finalText.length > 0);
76273
+ if (finalText.length > 0) {
76274
+ await append(finalText, "system");
76275
+ finish(finalText, true);
76276
+ } else {
76277
+ const text87 = `[${getBrandLogTag()}: no response]`;
76278
+ await append(text87);
76279
+ finish(text87, false);
76280
+ }
75803
76281
  } catch (err) {
75804
76282
  const text87 = `[${getBrandLogTag()}: ${err.message ?? "dispatch failed"} \u2014 try after the call]`;
75805
76283
  await append(text87);
@@ -75842,12 +76320,14 @@ function extractFinalText(cliStdout) {
75842
76320
  }
75843
76321
  return trimmed;
75844
76322
  }
75845
- var DISPATCH_MAX_TURNS, DISPATCH_TIMEOUT_MS, DISPATCH_MODEL, PENDING_RESULT_TTL_MS, pendingResults, SYSTEM_PROMPT3;
76323
+ var DISPATCH_MAX_TURNS, DISPATCH_TIMEOUT_MS, DISPATCH_MODEL, PENDING_RESULT_TTL_MS, pendingResults, SYSTEM_PROMPT4;
75846
76324
  var init_voice_intent_dispatcher = __esm({
75847
76325
  "../server/src/services/voice-intent-dispatcher.ts"() {
75848
76326
  "use strict";
75849
76327
  init_dist2();
75850
76328
  init_server2();
76329
+ init_ui();
76330
+ init_dist();
75851
76331
  init_issue_suggestions();
75852
76332
  init_agent_auth_jwt();
75853
76333
  init_brand2();
@@ -75857,10 +76337,14 @@ var init_voice_intent_dispatcher = __esm({
75857
76337
  DISPATCH_MODEL = "claude-haiku-4-5-20251001";
75858
76338
  PENDING_RESULT_TTL_MS = 5 * 60 * 1e3;
75859
76339
  pendingResults = /* @__PURE__ */ new Map();
75860
- SYSTEM_PROMPT3 = `You are speaking with a Paperclip operator on a live voice call. They just asked you to read or change something via the live-call paperclip tool. Run any reads/writes you need against the local API using the bundled paperclip skill, then return ONE final plain-text answer your voice can speak back conversationally \u2014 at most two short sentences, no markdown, no JSON, no list bullets.
76340
+ SYSTEM_PROMPT4 = `You are speaking with a Paperclip operator on a live voice call. They just asked you to read or change something via the live-call paperclip tool. Run any reads/writes you need against the local API using the bundled paperclip skill, then return ONE final plain-text answer your voice can speak back conversationally \u2014 at most two short sentences, no markdown, no JSON, no list bullets.
75861
76341
 
75862
76342
  If the intent has multiple parts (e.g. "cancel and add a comment", "update status and notify the assignee", "close the issue and leave a note"), you MUST do EVERY part. Each write is a separate API call \u2014 don't skip the second half just because the first one succeeded. If a part can't be done, say which part failed in your final answer.
75863
76343
 
76344
+ For content / posts / published-state questions ("what content did we post today?", "what's scheduled?", "did the X post go out?"), query GET /api/companies/{companyId}/content (see the paperclip-extras skill for filters and the postedAt/scheduledAt fields \u2014 list endpoint returns a bare JSON array, status enum is drafted/in_review/approved/scheduled/publishing/posted/failed/cancelled). NEVER answer content questions from issue data \u2014 content and issues are different entities.
76345
+
76346
+ Time handling: server timestamps are UTC ISO strings. The operator's local timezone is injected into your prompt below. ALWAYS convert UTC timestamps to that timezone before stating them. Never say "02:38" when the operator is in Bangkok and the local time was 09:38.
76347
+
75864
76348
  Style: like a teammate giving a quick verbal answer. If you did a write, confirm it crisply and name what you changed. If you read state, name the top 1-3 items. Skip preamble like "I checked" or "Here's what I found" \u2014 just say it. If you couldn't do it, say so honestly in one sentence.
75865
76349
 
75866
76350
  Writes attribute to you, the agent the operator is talking to.`;
@@ -76664,7 +77148,7 @@ var init_plugin_config_secret_normalizer = __esm({
76664
77148
  // ../server/src/routes/plugins.ts
76665
77149
  import { existsSync as existsSync10, readFileSync as readFileSync6 } from "node:fs";
76666
77150
  import path64 from "node:path";
76667
- import { randomUUID as randomUUID15 } from "node:crypto";
77151
+ import { randomUUID as randomUUID17 } from "node:crypto";
76668
77152
  import { fileURLToPath as fileURLToPath19 } from "node:url";
76669
77153
  import { Router as Router36 } from "express";
76670
77154
  import { and as and60, desc as desc35, eq as eq63, gte as gte12 } from "drizzle-orm";
@@ -77862,7 +78346,7 @@ function pluginRoutes(db, loader, jobDeps, webhookDeps, toolDeps, bridgeDeps) {
77862
78346
  });
77863
78347
  return;
77864
78348
  }
77865
- const requestId = randomUUID15();
78349
+ const requestId = randomUUID17();
77866
78350
  const rawHeaders = {};
77867
78351
  for (const [key, value] of Object.entries(req.headers)) {
77868
78352
  if (typeof value === "string") {
@@ -78042,6 +78526,7 @@ function pluginRoutes(db, loader, jobDeps, webhookDeps, toolDeps, bridgeDeps) {
78042
78526
  const intent = typeof body?.intent === "string" ? body.intent : "";
78043
78527
  const surfaceFromBody = body?.surface === "runway" || body?.surface === "realtime-voice" || body?.surface === "voice" ? body.surface : void 0;
78044
78528
  const correlationId = typeof body?.correlationId === "string" && body.correlationId.trim().length > 0 ? body.correlationId.trim() : void 0;
78529
+ const operatorTimezone = typeof body?.operatorTimezone === "string" && body.operatorTimezone.trim().length > 0 ? body.operatorTimezone.trim() : void 0;
78045
78530
  if (!companyId || !agentId || !chatSessionId || !intent) {
78046
78531
  res.status(400).json({ error: "companyId, agentId, chatSessionId, and intent are required" });
78047
78532
  return;
@@ -78065,15 +78550,41 @@ function pluginRoutes(db, loader, jobDeps, webhookDeps, toolDeps, bridgeDeps) {
78065
78550
  }
78066
78551
  );
78067
78552
  } : void 0;
78553
+ const onToolEvent = bridgeDeps?.streamBus && voicePluginRecord ? (event) => {
78554
+ bridgeDeps.streamBus.publish(
78555
+ voicePluginRecord.id,
78556
+ VOICE_INTENT_RESULT_CHANNEL2,
78557
+ event.companyId,
78558
+ {
78559
+ kind: "tool_event",
78560
+ chatSessionId: event.chatSessionId,
78561
+ agentId: event.agentId,
78562
+ correlationId: event.correlationId,
78563
+ event: event.kind === "tool_call" ? {
78564
+ kind: "tool_call",
78565
+ id: event.id,
78566
+ name: event.name,
78567
+ input: event.input
78568
+ } : {
78569
+ kind: "tool_result",
78570
+ id: event.id,
78571
+ content: event.content,
78572
+ isError: event.isError
78573
+ },
78574
+ at: (/* @__PURE__ */ new Date()).toISOString()
78575
+ }
78576
+ );
78577
+ } : void 0;
78068
78578
  void dispatchVoiceIntent(
78069
- { db, onComplete },
78579
+ { db, onComplete, onToolEvent },
78070
78580
  {
78071
78581
  companyId,
78072
78582
  agentId,
78073
78583
  chatSessionId,
78074
78584
  intent,
78075
78585
  surface: surfaceFinal,
78076
- correlationId
78586
+ correlationId,
78587
+ operatorTimezone
78077
78588
  }
78078
78589
  ).catch(() => {
78079
78590
  });
@@ -78809,7 +79320,7 @@ var init_plugin_ui_static = __esm({
78809
79320
  });
78810
79321
 
78811
79322
  // ../server/src/ui-branding.ts
78812
- function isTruthyEnvValue(value) {
79323
+ function isTruthyEnvValue2(value) {
78813
79324
  if (!value) return false;
78814
79325
  const normalized = value.trim().toLowerCase();
78815
79326
  return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
@@ -78906,6 +79417,15 @@ function createFaviconDataUrl(background, foreground) {
78906
79417
  ].join("");
78907
79418
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
78908
79419
  }
79420
+ function createNeutralFaviconDataUrl() {
79421
+ const svg = [
79422
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">',
79423
+ '<rect width="24" height="24" rx="6" fill="#1f2937"/>',
79424
+ '<circle cx="12" cy="12" r="3" fill="#e5e7eb"/>',
79425
+ "</svg>"
79426
+ ].join("");
79427
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
79428
+ }
78909
79429
  function createInitialsFaviconDataUrl(brandName) {
78910
79430
  const initials = brandName.trim().slice(0, 2).toUpperCase();
78911
79431
  const bg = deriveColorFromSeed(brandName);
@@ -78919,7 +79439,7 @@ function createInitialsFaviconDataUrl(brandName) {
78919
79439
  return `data:image/svg+xml,${encodeURIComponent(svg)}`;
78920
79440
  }
78921
79441
  function isWorktreeUiBrandingEnabled(env = process.env) {
78922
- return isTruthyEnvValue(env.PAPERCLIP_IN_WORKTREE);
79442
+ return isTruthyEnvValue2(env.PAPERCLIP_IN_WORKTREE);
78923
79443
  }
78924
79444
  function getWorktreeUiBranding(env = process.env) {
78925
79445
  if (!isWorktreeUiBrandingEnabled(env)) {
@@ -78952,15 +79472,17 @@ function renderFaviconLinksFromHref(faviconHref) {
78952
79472
  }
78953
79473
  function renderRuntimeBrandingMeta(branding, env = process.env) {
78954
79474
  const brand2 = getBrandPack(env);
79475
+ const displayName = brand2.hideBrand ? HIDDEN_BRAND_TITLE : brand2.name;
78955
79476
  const lines = [
78956
- `<meta name="apple-mobile-web-app-title" content="${escapeHtmlAttribute(brand2.name)}" />`,
78957
- `<title>${escapeHtmlAttribute(brand2.name)}</title>`,
79477
+ `<meta name="apple-mobile-web-app-title" content="${escapeHtmlAttribute(displayName)}" />`,
79478
+ `<title>${escapeHtmlAttribute(displayName)}</title>`,
78958
79479
  `<meta name="paperclip-brand-name" content="${escapeHtmlAttribute(brand2.name)}" />`,
78959
79480
  `<meta name="paperclip-brand-logo-url" content="${escapeHtmlAttribute(brand2.logoUrl ?? "")}" />`,
78960
79481
  `<meta name="paperclip-brand-homepage" content="${escapeHtmlAttribute(brand2.homepageUrl ?? "")}" />`,
78961
79482
  `<meta name="paperclip-brand-update-url" content="${escapeHtmlAttribute(brand2.updateUrl ?? "")}" />`,
78962
79483
  `<meta name="paperclip-brand-support-email" content="${escapeHtmlAttribute(brand2.supportEmail ?? "")}" />`,
78963
- `<meta name="paperclip-brand-docs" content="${escapeHtmlAttribute(brand2.docsUrl ?? "")}" />`
79484
+ `<meta name="paperclip-brand-docs" content="${escapeHtmlAttribute(brand2.docsUrl ?? "")}" />`,
79485
+ `<meta name="paperclip-brand-hide" content="${brand2.hideBrand ? "true" : "false"}" />`
78964
79486
  ];
78965
79487
  if (branding.enabled && branding.name && branding.color && branding.textColor) {
78966
79488
  lines.push('<meta name="paperclip-worktree-enabled" content="true" />');
@@ -78984,10 +79506,15 @@ ${content.split("\n").map((line) => ` ${line}`).join("\n")}
78984
79506
  function applyUiBranding(html, env = process.env) {
78985
79507
  const branding = getWorktreeUiBranding(env);
78986
79508
  const brand2 = getBrandPack(env);
78987
- let faviconHref = branding.enabled ? branding.faviconHref : null;
79509
+ let faviconHref = null;
79510
+ if (brand2.hideBrand) {
79511
+ faviconHref = createNeutralFaviconDataUrl();
79512
+ } else if (branding.enabled) {
79513
+ faviconHref = branding.faviconHref;
79514
+ }
78988
79515
  if (!faviconHref && brand2.logoUrl) {
78989
79516
  faviconHref = brand2.logoUrl;
78990
- } else if (!faviconHref && brand2.name !== "Paperclip") {
79517
+ } else if (!faviconHref && !brand2.hideBrand && brand2.name !== "Paperclip") {
78991
79518
  faviconHref = createInitialsFaviconDataUrl(brand2.name);
78992
79519
  }
78993
79520
  const withFavicon = replaceMarkedBlock(html, FAVICON_BLOCK_START, FAVICON_BLOCK_END, renderFaviconLinksFromHref(faviconHref));
@@ -78998,7 +79525,7 @@ function applyUiBranding(html, env = process.env) {
78998
79525
  renderRuntimeBrandingMeta(branding, env)
78999
79526
  );
79000
79527
  }
79001
- var FAVICON_BLOCK_START, FAVICON_BLOCK_END, RUNTIME_BRANDING_BLOCK_START, RUNTIME_BRANDING_BLOCK_END, DEFAULT_FAVICON_LINKS;
79528
+ var FAVICON_BLOCK_START, FAVICON_BLOCK_END, RUNTIME_BRANDING_BLOCK_START, RUNTIME_BRANDING_BLOCK_END, DEFAULT_FAVICON_LINKS, HIDDEN_BRAND_TITLE;
79002
79529
  var init_ui_branding = __esm({
79003
79530
  "../server/src/ui-branding.ts"() {
79004
79531
  "use strict";
@@ -79013,6 +79540,7 @@ var init_ui_branding = __esm({
79013
79540
  '<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />',
79014
79541
  '<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />'
79015
79542
  ].join("\n");
79543
+ HIDDEN_BRAND_TITLE = "App";
79016
79544
  }
79017
79545
  });
79018
79546
 
@@ -79762,7 +80290,7 @@ var init_plugin_stream_bus = __esm({
79762
80290
  });
79763
80291
 
79764
80292
  // ../server/src/services/plugin-job-scheduler.ts
79765
- import { and as and61, eq as eq64, lte as lte9, or as or14 } from "drizzle-orm";
80293
+ import { and as and61, eq as eq64, lte as lte9, or as or15 } from "drizzle-orm";
79766
80294
  function createPluginJobScheduler(options2) {
79767
80295
  const {
79768
80296
  db,
@@ -80059,7 +80587,7 @@ function createPluginJobScheduler(options2) {
80059
80587
  const runningRuns = await db.select().from(pluginJobRuns).where(
80060
80588
  and61(
80061
80589
  eq64(pluginJobRuns.pluginId, pluginId),
80062
- or14(
80590
+ or15(
80063
80591
  eq64(pluginJobRuns.status, "running"),
80064
80592
  eq64(pluginJobRuns.status, "queued")
80065
80593
  )
@@ -80944,7 +81472,8 @@ function pluginChatHostBridge(db) {
80944
81472
  role: input.role,
80945
81473
  content: input.content,
80946
81474
  stopReason: input.stopReason ?? null,
80947
- usage: input.usage ?? null
81475
+ usage: input.usage ?? null,
81476
+ createdAt: input.createdAt
80948
81477
  });
80949
81478
  return {
80950
81479
  id: row2.id,
@@ -81260,7 +81789,7 @@ var init_plugin_state_store = __esm({
81260
81789
 
81261
81790
  // ../server/src/services/plugin-host-services.ts
81262
81791
  import { eq as eq68, and as and65, like, desc as desc37 } from "drizzle-orm";
81263
- import { randomUUID as randomUUID16 } from "node:crypto";
81792
+ import { randomUUID as randomUUID18 } from "node:crypto";
81264
81793
  import { lookup as dnsLookup } from "node:dns/promises";
81265
81794
  import { request as httpRequest } from "node:http";
81266
81795
  import { request as httpsRequest } from "node:https";
@@ -81951,7 +82480,8 @@ function buildHostServices(db, pluginId, pluginKey, eventBus, notifyWorker) {
81951
82480
  role: params.role,
81952
82481
  content,
81953
82482
  stopReason: params.stopReason ?? null,
81954
- usage: params.usage ?? null
82483
+ usage: params.usage ?? null,
82484
+ createdAt: params.createdAt
81955
82485
  });
81956
82486
  },
81957
82487
  async listMessages(params) {
@@ -81987,7 +82517,7 @@ function buildHostServices(db, pluginId, pluginKey, eventBus, notifyWorker) {
81987
82517
  await ensurePluginAvailableForCompany(companyId);
81988
82518
  const agent = await agents2.getById(params.agentId);
81989
82519
  requireInCompany("Agent", agent, companyId);
81990
- const taskKey = params.taskKey ?? `plugin:${pluginKey}:session:${randomUUID16()}`;
82520
+ const taskKey = params.taskKey ?? `plugin:${pluginKey}:session:${randomUUID18()}`;
81991
82521
  const row2 = await db.insert(agentTaskSessions).values({
81992
82522
  companyId,
81993
82523
  agentId: params.agentId,