@sentry/junior 0.143.0 → 0.145.0

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 (100) hide show
  1. package/dist/{agent-hooks-2EP3QS47.js → agent-hooks-L7D2BDVV.js} +10 -10
  2. package/dist/api/conversations/detail.d.ts +9 -0
  3. package/dist/api/schema/conversation.d.ts +20 -0
  4. package/dist/api/schema/task.d.ts +40 -1
  5. package/dist/api/schema.d.ts +3 -3
  6. package/dist/api/schema.js +5 -1
  7. package/dist/api.js +2250 -2172
  8. package/dist/app.js +335 -409
  9. package/dist/{catalog-runtime-IAVW4W3B.js → catalog-runtime-QLAC4Q3W.js} +5 -5
  10. package/dist/chat/agent/request.d.ts +1 -1
  11. package/dist/chat/agent/resume.d.ts +6 -15
  12. package/dist/chat/agent/tools.d.ts +1 -1
  13. package/dist/chat/agent-dispatch/types.d.ts +1 -1
  14. package/dist/chat/app/conversation-work.d.ts +24 -0
  15. package/dist/chat/conversations/projection.d.ts +11 -3
  16. package/dist/chat/conversations/store.d.ts +1 -1
  17. package/dist/chat/runtime/agent-run-outcome.d.ts +1 -1
  18. package/dist/chat/runtime/reply-executor.d.ts +5 -5
  19. package/dist/chat/runtime/slack-resume.d.ts +12 -4
  20. package/dist/chat/scheduled-tasks/store.d.ts +1 -0
  21. package/dist/chat/scheduled-tasks/tool-support.d.ts +26 -1
  22. package/dist/chat/scheduled-tasks/tools/create.d.ts +10 -0
  23. package/dist/chat/scheduled-tasks/tools/delete.d.ts +10 -0
  24. package/dist/chat/scheduled-tasks/tools/list.d.ts +15 -2
  25. package/dist/chat/scheduled-tasks/tools/run-now.d.ts +10 -0
  26. package/dist/chat/scheduled-tasks/tools/update.d.ts +12 -1
  27. package/dist/chat/services/guardian-action-policy.d.ts +1 -1
  28. package/dist/chat/state/locks.d.ts +28 -0
  29. package/dist/chat/task-execution/checkpoint.d.ts +86 -0
  30. package/dist/chat/task-execution/paused-turn.d.ts +29 -0
  31. package/dist/chat/task-execution/queue-signing.d.ts +1 -0
  32. package/dist/chat/task-execution/queue.d.ts +7 -1
  33. package/dist/chat/task-execution/slack-work.d.ts +1 -1
  34. package/dist/chat/task-execution/state.d.ts +4 -4
  35. package/dist/chat/task-execution/store.d.ts +2 -2
  36. package/dist/chat/task-execution/turn-cursor-keys.d.ts +6 -0
  37. package/dist/chat/{state/turn-session.d.ts → task-execution/turn-cursor.d.ts} +52 -42
  38. package/dist/chat/task-execution/turn-wake.d.ts +29 -0
  39. package/dist/chat/tasks/execution-stats.d.ts +21 -0
  40. package/dist/chat/tasks/read.d.ts +9 -1
  41. package/dist/chat/tools/types.d.ts +1 -1
  42. package/dist/{chunk-2KLMGRRQ.js → chunk-27TEJTZO.js} +1 -1
  43. package/dist/{chunk-GZRWULXF.js → chunk-2LWPFSB4.js} +1 -1
  44. package/dist/{chunk-NAINBOZQ.js → chunk-2UH3JI64.js} +10 -9
  45. package/dist/{chunk-QFWXGUAH.js → chunk-2XET6PKP.js} +93 -89
  46. package/dist/{chunk-U5ATCOHE.js → chunk-5PNPQYTO.js} +2 -2
  47. package/dist/{chunk-XYJNNZYR.js → chunk-64SBYW2O.js} +397 -288
  48. package/dist/{chunk-F6RWNJBK.js → chunk-B46ZMVOV.js} +5 -5
  49. package/dist/{chunk-BJ6ISBOA.js → chunk-CD2XFGYQ.js} +1 -1
  50. package/dist/{chunk-WSWVWP2V.js → chunk-DS3J6WGY.js} +1 -1
  51. package/dist/{chunk-OG5JQHKR.js → chunk-DXEQAIUV.js} +1 -1
  52. package/dist/{chunk-A3XPSABC.js → chunk-EIX2LQ6L.js} +82 -4
  53. package/dist/{chunk-ANG7IWAI.js → chunk-FUBRJA5F.js} +6 -6
  54. package/dist/{chunk-OB7ZJK7Y.js → chunk-GDV5LXRA.js} +1 -1
  55. package/dist/{chunk-AEFJN7OK.js → chunk-GUWLDMGF.js} +4 -4
  56. package/dist/{chunk-I4NQNZVW.js → chunk-HISR3DTV.js} +33 -61
  57. package/dist/{chunk-WM4PRM7W.js → chunk-HLOUZC5C.js} +32 -34
  58. package/dist/{chunk-RABHACKG.js → chunk-I47UGQPJ.js} +7 -7
  59. package/dist/{chunk-IUFM5JWV.js → chunk-JAL3CH4G.js} +3 -0
  60. package/dist/{chunk-OGQGJAD7.js → chunk-JLMVCNXL.js} +26 -1
  61. package/dist/{chunk-5YJBS4I5.js → chunk-JTGS3ZZE.js} +1 -1
  62. package/dist/{chunk-LSBQHDI2.js → chunk-LVVSIMBB.js} +1 -1
  63. package/dist/{chunk-2T5CHXRZ.js → chunk-PJ4NMVQB.js} +2 -2
  64. package/dist/{chunk-I5PIM72M.js → chunk-QJBDABQA.js} +14 -17
  65. package/dist/{chunk-M2T7MHQJ.js → chunk-QUG4JSZK.js} +43 -1
  66. package/dist/{chunk-X5SC2UGE.js → chunk-RUSS62EN.js} +14 -14
  67. package/dist/{chunk-NVFVM67W.js → chunk-SIN3MXH6.js} +1 -1
  68. package/dist/{chunk-I25V3XRN.js → chunk-TZRXYWRE.js} +2 -2
  69. package/dist/{chunk-WJHWOAVZ.js → chunk-VTDMB7XX.js} +1 -1
  70. package/dist/{chunk-TPJK6AVJ.js → chunk-WF5HVH3M.js} +1 -1
  71. package/dist/{chunk-KCBZSJRU.js → chunk-WR76IVA5.js} +5 -2
  72. package/dist/{chunk-7GVX5SOG.js → chunk-XFKIRN3Q.js} +29 -29
  73. package/dist/{chunk-UFCA4EZB.js → chunk-YD54Q45G.js} +4 -4
  74. package/dist/{chunk-PBV7VXUA.js → chunk-ZIAMN2UW.js} +407 -523
  75. package/dist/{chunk-SNBTWIQO.js → chunk-ZIMRGFYL.js} +6 -6
  76. package/dist/{chunk-STX5WIH6.js → chunk-ZYAJMESY.js} +2 -2
  77. package/dist/cli/chat.js +32 -32
  78. package/dist/cli/check.js +6 -6
  79. package/dist/cli/plugins.js +13 -13
  80. package/dist/cli/snapshot-create.js +9 -9
  81. package/dist/cli/upgrade.js +7 -7
  82. package/dist/{db-RBXBVEVO.js → db-62PW6LM3.js} +7 -7
  83. package/dist/instrumentation.js +4 -4
  84. package/dist/nitro.js +4 -4
  85. package/dist/{oauth-callback-server-LRB6BX6C.js → oauth-callback-server-6NZNHJAM.js} +25 -25
  86. package/dist/{runner-WK5VUOW6.js → runner-GCRPG3GQ.js} +19 -18
  87. package/dist/{skills-MJWRME3A.js → skills-2S3MDFIQ.js} +6 -6
  88. package/dist/spawn-7WK37BTD.js +39 -0
  89. package/dist/{validation-556WSWVU.js → validation-HAOLCJ5A.js} +6 -6
  90. package/dist/version.js +1 -1
  91. package/dist/{work-W6IXSPY3.js → work-R4XYKMUW.js} +17 -16
  92. package/dist/worker-U2UCVR7M.js +24 -0
  93. package/package.json +5 -5
  94. package/dist/chat/agent/session.d.ts +0 -19
  95. package/dist/chat/runtime/agent-continue-runner.d.ts +0 -37
  96. package/dist/chat/services/agent-continue.d.ts +0 -29
  97. package/dist/chat/services/turn-session-record.d.ts +0 -164
  98. package/dist/chat/state/turn-session-keys.d.ts +0 -6
  99. package/dist/spawn-ZCW6UOAA.js +0 -38
  100. package/dist/worker-5KM346HH.js +0 -24
package/dist/api.js CHANGED
@@ -27,22 +27,25 @@ import {
27
27
  statsReportSchema,
28
28
  taskExecutionListSchema,
29
29
  taskListSchema,
30
- taskParamsSchema
31
- } from "./chunk-OGQGJAD7.js";
30
+ taskParamsSchema,
31
+ taskRunListSchema
32
+ } from "./chunk-JLMVCNXL.js";
32
33
  import {
33
34
  formatSlackConversationRedactedLabel,
34
35
  readHealthReport,
35
36
  resolveSlackConversationContextFromThreadId
36
- } from "./chunk-2T5CHXRZ.js";
37
+ } from "./chunk-PJ4NMVQB.js";
37
38
  import {
38
39
  buildSentryConversationUrl
39
40
  } from "./chunk-4CODA7P5.js";
40
41
  import {
42
+ readTaskExecutionByConversationId,
41
43
  readTaskExecutionDays,
42
44
  readTaskExecutionStatusDays,
43
45
  readTaskExecutionSummaries,
44
- readTaskExecutions
45
- } from "./chunk-A3XPSABC.js";
46
+ readTaskExecutions,
47
+ readTaskRuns
48
+ } from "./chunk-EIX2LQ6L.js";
46
49
  import {
47
50
  healthReportSchema,
48
51
  pluginOperationalReportFeedSchema,
@@ -61,13 +64,13 @@ import {
61
64
  listPublicEventTasksForTeams,
62
65
  listPublicScheduledTasksForTeams,
63
66
  readStats
64
- } from "./chunk-UFCA4EZB.js";
67
+ } from "./chunk-YD54Q45G.js";
65
68
  import {
66
69
  parseSlackThreadId
67
- } from "./chunk-OG5JQHKR.js";
70
+ } from "./chunk-DXEQAIUV.js";
68
71
  import {
69
72
  fallbackShortTitle
70
- } from "./chunk-STX5WIH6.js";
73
+ } from "./chunk-ZYAJMESY.js";
71
74
  import {
72
75
  agentTurnUsageSchema,
73
76
  hasAgentTurnUsage,
@@ -80,44 +83,44 @@ import {
80
83
  listConversationAnnotations,
81
84
  renderPluginConversationEvent,
82
85
  resolveViewerUser
83
- } from "./chunk-SNBTWIQO.js";
86
+ } from "./chunk-ZIMRGFYL.js";
84
87
  import {
85
88
  getSlackClient,
86
89
  withSlackRetries
87
- } from "./chunk-I25V3XRN.js";
90
+ } from "./chunk-TZRXYWRE.js";
88
91
  import {
89
92
  decodeStoredConversationEvent,
90
93
  getDb,
91
94
  getSqlExecutor,
92
95
  locationFromRow,
93
96
  parseSessionSource
94
- } from "./chunk-NAINBOZQ.js";
97
+ } from "./chunk-2UH3JI64.js";
95
98
  import {
96
99
  juniorApiTokens,
97
100
  juniorConversationEvents,
98
101
  juniorConversations,
99
102
  juniorIdentities,
100
103
  juniorUsers
101
- } from "./chunk-2KLMGRRQ.js";
104
+ } from "./chunk-27TEJTZO.js";
102
105
  import "./chunk-EM225ITJ.js";
103
- import "./chunk-M2T7MHQJ.js";
104
- import "./chunk-7GVX5SOG.js";
106
+ import "./chunk-QUG4JSZK.js";
107
+ import "./chunk-XFKIRN3Q.js";
105
108
  import {
106
109
  juniorDestinations
107
110
  } from "./chunk-VFUK3X5B.js";
108
111
  import {
109
112
  canExposeConversationPayload
110
- } from "./chunk-5YJBS4I5.js";
113
+ } from "./chunk-JTGS3ZZE.js";
111
114
  import "./chunk-HZF5P5UN.js";
112
115
  import {
113
116
  parseSlackChannelId,
114
117
  parseSlackMessageTs,
115
118
  parseSlackTeamId
116
119
  } from "./chunk-F3B2YJ76.js";
117
- import "./chunk-GZRWULXF.js";
120
+ import "./chunk-2LWPFSB4.js";
118
121
  import "./chunk-6GWA276C.js";
119
- import "./chunk-WSWVWP2V.js";
120
- import "./chunk-OB7ZJK7Y.js";
122
+ import "./chunk-DS3J6WGY.js";
123
+ import "./chunk-GDV5LXRA.js";
121
124
  import "./chunk-MLKGABMK.js";
122
125
 
123
126
  // src/api.ts
@@ -1613,2356 +1616,2427 @@ var list_default = defineApiRoute({
1613
1616
  }
1614
1617
  });
1615
1618
 
1616
- // src/api/conversations/detail.ts
1617
- function projectConversationDetail(args) {
1618
- const { conversation } = args;
1619
- const conversationId = conversation.conversationId;
1620
- const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
1621
- const canExposePayload = args.access?.canViewPrivateContent ?? false;
1622
- const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
1623
- const sentryConversationUrl = buildSentryConversationUrl(conversationId);
1624
- return {
1625
- ...conversationSummaryFromStoredConversation({
1626
- access: args.access,
1627
- auxiliaryCosts: args.auxiliaryCosts,
1628
- conversation,
1629
- durationMs: args.durationMs,
1630
- ...args.locationId ? { locationId: args.locationId } : {},
1631
- ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : {},
1632
- usage: args.usage
1633
- }),
1634
- annotations: canExposePayload ? args.annotations : [],
1635
- events: args.events,
1636
- ...args.previousSeq !== void 0 ? {
1637
- previousCursor: encodeConversationCursor({
1638
- conversationId,
1639
- seq: args.previousSeq
1640
- })
1641
- } : {},
1642
- ...modelUsage.length > 0 ? { modelUsage } : {},
1643
- eventHistory: conversationEventHistory({
1644
- canExposePayload,
1645
- ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
1646
- }),
1647
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1648
- ...sentryConversationUrl ? { sentryConversationUrl } : {}
1649
- };
1650
- }
1651
- async function readConversationDetailFromSql(conversationId, options) {
1652
- const record = await readConversationRecordFromSql(conversationId);
1653
- if (!record) return void 0;
1654
- const executor = getSqlExecutor();
1655
- const includeDescendantMetrics = record.rootConversationId === conversationId;
1656
- const [
1657
- accessByConversation,
1658
- annotations,
1659
- auxiliaryCostsByConversation,
1660
- modelUsage,
1661
- metricsByRoot,
1662
- teamDomainByTeamId
1663
- ] = await Promise.all([
1664
- readConversationAccessFromSql(
1665
- getDb(),
1666
- [conversationId],
1667
- options.verifiedViewerEmail
1668
- ),
1669
- listConversationAnnotations(getDb(), conversationId),
1670
- readConversationAuxiliaryCostsFromSql(getDb(), [conversationId], {
1671
- includeDescendants: includeDescendantMetrics
1672
- }),
1673
- record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, {
1674
- conversationId,
1675
- includeDescendants: includeDescendantMetrics
1676
- }) : Promise.resolve([]),
1677
- readRootConversationMetricsFromSql(
1678
- getDb(),
1679
- includeDescendantMetrics ? [conversationId] : []
1680
- ),
1681
- resolveSlackTeamDomains(
1682
- record.conversation.sessionSource?.platform === "slack" ? [record.conversation.sessionSource.teamId] : []
1683
- )
1684
- ]);
1685
- const access = accessByConversation.get(conversationId);
1686
- const page = record.conversation.transcriptPurgedAtMs === void 0 ? await readConversationEventPage(executor, {
1687
- canExposePayload: access?.canViewPrivateContent ?? false,
1688
- conversationId,
1689
- limit: options.limit
1690
- }) : { events: [] };
1691
- const metrics = metricsByRoot.get(conversationId);
1692
- return projectConversationDetail({
1693
- ...record,
1694
- access,
1695
- annotations,
1696
- auxiliaryCosts: auxiliaryCostsByConversation.get(conversationId),
1697
- durationMs: metrics?.durationMs ?? record.durationMs,
1698
- events: page.events,
1699
- modelUsage,
1700
- teamDomainByTeamId,
1701
- ...page.previousSeq === void 0 ? {} : { previousSeq: page.previousSeq },
1702
- usage: metrics?.usage ?? record.usage ?? void 0
1703
- });
1704
- }
1705
- async function readConversationDetail(conversationId, options = {}) {
1706
- const report = await readConversationDetailFromSql(conversationId, {
1707
- ...options,
1708
- limit: options.limit ?? 500
1709
- });
1710
- return report ? conversationDetailReportSchema.parse(report) : void 0;
1619
+ // src/chat/tasks/read.ts
1620
+ import { and as and7, eq as eq8, or as or2 } from "drizzle-orm";
1621
+
1622
+ // src/chat/scheduled-tasks/personal.ts
1623
+ import { z as z3 } from "zod";
1624
+ var cursorSchema = z3.object({
1625
+ createdAtMs: z3.number().finite(),
1626
+ id: z3.string().min(1),
1627
+ query: z3.string().max(200).optional(),
1628
+ version: z3.literal(1)
1629
+ }).strict();
1630
+ var InvalidScheduledTaskCursorError = class extends Error {
1631
+ constructor() {
1632
+ super("Scheduled task cursor is invalid.");
1633
+ this.name = "InvalidScheduledTaskCursorError";
1634
+ }
1635
+ };
1636
+ var PersonalScheduledTaskNotFoundError = class extends Error {
1637
+ constructor() {
1638
+ super("Scheduled task was not found.");
1639
+ this.name = "PersonalScheduledTaskNotFoundError";
1640
+ }
1641
+ };
1642
+ function normalizeQuery(query) {
1643
+ return query?.trim().toLowerCase() || void 0;
1711
1644
  }
1712
- var detail_default = defineApiRoute({
1713
- method: "get",
1714
- path: "/:conversationId",
1715
- responseSchema: conversationDetailReportSchema,
1716
- handler: async (c) => {
1717
- const { conversationId } = parseParams(
1718
- conversationParamsSchema,
1719
- c.req.param()
1645
+ function decodeCursor(value, query) {
1646
+ if (!value) return void 0;
1647
+ try {
1648
+ const parsed = cursorSchema.parse(
1649
+ JSON.parse(Buffer.from(value, "base64url").toString("utf8"))
1720
1650
  );
1721
- const query = parseQuery(conversationDetailQuerySchema, c.req.query());
1722
- const verifiedViewerEmail = c.get("verifiedViewerEmail");
1723
- const report = await readConversationDetail(conversationId, {
1724
- ...query,
1725
- ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
1726
- });
1727
- if (!report) throwApiError(404, "Conversation not found.");
1728
- return report;
1651
+ if (parsed.query !== query) {
1652
+ throw new InvalidScheduledTaskCursorError();
1653
+ }
1654
+ return { createdAtMs: parsed.createdAtMs, id: parsed.id };
1655
+ } catch {
1656
+ throw new InvalidScheduledTaskCursorError();
1729
1657
  }
1730
- });
1731
-
1732
- // src/api/conversations/event-list.ts
1733
- async function readConversationEvents(conversationId, beforeValue, options = {}) {
1734
- const record = await readConversationRecordFromSql(conversationId);
1735
- if (!record) return void 0;
1736
- const before = decodeConversationCursor({
1737
- conversationId,
1738
- cursor: beforeValue
1739
- });
1740
- if (!before) throwApiError(400, "Invalid conversation cursor.");
1741
- const accessByConversation = await readConversationAccessFromSql(
1742
- getDb(),
1743
- [conversationId],
1744
- options.verifiedViewerEmail
1745
- );
1746
- const access = accessByConversation.get(conversationId);
1747
- const canExposePayload = access?.canViewPrivateContent ?? false;
1748
- const transcriptPurgedAtMs = record.conversation.transcriptPurgedAtMs;
1749
- const page = transcriptPurgedAtMs === void 0 ? await readConversationEventPage(getSqlExecutor(), {
1750
- beforeSeq: before.seq,
1751
- canExposePayload,
1752
- conversationId,
1753
- limit: options.limit ?? 500
1754
- }) : { events: [] };
1755
- return conversationEventPageSchema.parse({
1756
- events: page.events,
1757
- eventHistory: conversationEventHistory({
1758
- canExposePayload,
1759
- ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
1658
+ }
1659
+ function encodeCursor(task, query) {
1660
+ return Buffer.from(
1661
+ JSON.stringify({
1662
+ createdAtMs: task.createdAtMs,
1663
+ id: task.id,
1664
+ ...query ? { query } : {},
1665
+ version: 1
1760
1666
  }),
1761
- ...page.previousSeq === void 0 ? {} : {
1762
- previousCursor: encodeConversationCursor({
1763
- conversationId,
1764
- seq: page.previousSeq
1765
- })
1667
+ "utf8"
1668
+ ).toString("base64url");
1669
+ }
1670
+ function createViewerScheduledTasks(store, user) {
1671
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
1672
+ return {
1673
+ async delete(id, nowMs = Date.now()) {
1674
+ const task = await store.getTask(id);
1675
+ if (!task || task.status === "deleted" || !identityIds.has(task.creatorIdentityId)) {
1676
+ throw new PersonalScheduledTaskNotFoundError();
1677
+ }
1678
+ await store.saveTask({
1679
+ ...task,
1680
+ nextRunAtMs: void 0,
1681
+ runNowAtMs: void 0,
1682
+ status: "deleted",
1683
+ updatedAtMs: nowMs
1684
+ });
1766
1685
  },
1767
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1768
- });
1686
+ async list(input) {
1687
+ const query = normalizeQuery(input.query);
1688
+ const cursor = decodeCursor(input.cursor, query);
1689
+ const matching = await store.listTasksCreatedBy({
1690
+ ...cursor ? { before: cursor } : {},
1691
+ identityIds: [...identityIds],
1692
+ limit: input.limit + 1,
1693
+ ...query ? { query } : {}
1694
+ });
1695
+ const tasks = matching.slice(0, input.limit);
1696
+ return {
1697
+ tasks,
1698
+ ...matching.length > input.limit && tasks.length > 0 ? { nextCursor: encodeCursor(tasks.at(-1), query) } : {}
1699
+ };
1700
+ }
1701
+ };
1769
1702
  }
1770
- var event_list_default = defineApiRoute({
1771
- method: "get",
1772
- path: "/:conversationId/events",
1773
- responseSchema: conversationEventPageSchema,
1774
- handler: async (c) => {
1775
- const { conversationId } = parseParams(
1776
- conversationParamsSchema,
1777
- c.req.param()
1778
- );
1779
- const { before, limit } = parseQuery(
1780
- conversationEventsQuerySchema,
1781
- c.req.query()
1782
- );
1783
- const verifiedViewerEmail = c.get("verifiedViewerEmail");
1784
- const report = await readConversationEvents(conversationId, before, {
1785
- limit,
1786
- ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
1787
- });
1788
- if (!report) throwApiError(404, "Conversation not found.");
1789
- return report;
1790
- }
1791
- });
1792
-
1793
- // src/api/conversations/stats.query.ts
1794
- import { and as and7, eq as eq8, gte, isNull as isNull4, lte, sql as sql6 } from "drizzle-orm";
1795
- import { alias as alias4 } from "drizzle-orm/pg-core";
1796
1703
 
1797
- // src/api/conversations/aggregate.ts
1798
- import { sql as sql5 } from "drizzle-orm";
1799
- function tokenValue(source) {
1800
- return sql5`
1801
- CASE
1802
- WHEN COALESCE(
1803
- ${source.usage}->>'inputTokens',
1804
- ${source.usage}->>'outputTokens',
1805
- ${source.usage}->>'cachedInputTokens',
1806
- ${source.usage}->>'cacheCreationTokens'
1807
- ) IS NOT NULL
1808
- THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
1809
- + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
1810
- + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
1811
- + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
1812
- WHEN ${source.usage}->>'totalTokens' IS NOT NULL
1813
- THEN (${source.usage}->>'totalTokens')::double precision
1814
- ELSE NULL
1815
- END
1816
- `;
1817
- }
1818
- function costValue(source) {
1819
- return sql5`
1820
- CASE
1821
- WHEN ${source.usage}->'cost'->>'total' IS NOT NULL
1822
- THEN (${source.usage}->'cost'->>'total')::double precision
1823
- WHEN COALESCE(
1824
- ${source.usage}->'cost'->>'input',
1825
- ${source.usage}->'cost'->>'output',
1826
- ${source.usage}->'cost'->>'cacheRead',
1827
- ${source.usage}->'cost'->>'cacheWrite'
1828
- ) IS NOT NULL
1829
- THEN COALESCE((${source.usage}->'cost'->>'input')::double precision, 0)
1830
- + COALESCE((${source.usage}->'cost'->>'output')::double precision, 0)
1831
- + COALESCE((${source.usage}->'cost'->>'cacheRead')::double precision, 0)
1832
- + COALESCE((${source.usage}->'cost'->>'cacheWrite')::double precision, 0)
1833
- ELSE NULL
1834
- END
1835
- `;
1704
+ // src/chat/tasks/read.ts
1705
+ var TASK_LIST_LIMIT = 100;
1706
+ var TASK_FETCH_LIMIT = TASK_LIST_LIMIT + 1;
1707
+ var TASK_EXECUTION_LIST_LIMIT = 100;
1708
+ function creatorLabel(creator) {
1709
+ return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
1836
1710
  }
1837
- function conversationAggregateColumns(sources) {
1838
- const metrics = sources?.metrics ?? juniorConversations;
1839
- const roots = sources?.roots ?? juniorConversations;
1840
- const conversationCount = sources ? sql5`COUNT(DISTINCT ${roots.conversationId})` : sql5`COUNT(*)`;
1841
- return {
1842
- active: sql5`${conversationCount} FILTER (
1843
- WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
1844
- )::integer`,
1845
- conversations: sql5`${conversationCount}::integer`,
1846
- costUsd: sql5`SUM(${costValue(metrics)})::double precision`,
1847
- durationMs: sql5`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
1848
- failed: sql5`${conversationCount} FILTER (
1849
- WHERE ${roots.executionStatus} = 'failed'
1850
- )::integer`,
1851
- tokens: sql5`SUM(${tokenValue(metrics)})::double precision`
1852
- };
1711
+ function creatorKey(teamId, slackUserId) {
1712
+ return `${teamId}:${slackUserId}`;
1853
1713
  }
1854
- function conversationRangeColumns() {
1855
- return {
1856
- firstSeenAt: sql5`MIN(${juniorConversations.createdAt})`.mapWith(
1857
- juniorConversations.createdAt
1858
- ),
1859
- lastSeenAt: sql5`MAX(${juniorConversations.lastActivityAt})`.mapWith(
1860
- juniorConversations.lastActivityAt
1714
+ async function creatorProfileEmails(candidates) {
1715
+ const selectors = new Map(
1716
+ candidates.map(({ task }) => {
1717
+ const teamId = task.destination.teamId;
1718
+ const slackUserId = task.createdBy.slackUserId;
1719
+ return [
1720
+ creatorKey(teamId, slackUserId),
1721
+ { slackUserId, teamId }
1722
+ ];
1723
+ })
1724
+ );
1725
+ if (selectors.size === 0) return /* @__PURE__ */ new Map();
1726
+ const rows = await getDb().select({
1727
+ email: juniorUsers.primaryEmailNormalized,
1728
+ slackUserId: juniorIdentities.providerSubjectId,
1729
+ teamId: juniorIdentities.providerTenantId
1730
+ }).from(juniorIdentities).innerJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(
1731
+ and7(
1732
+ eq8(juniorIdentities.kind, "user"),
1733
+ eq8(juniorIdentities.provider, "slack"),
1734
+ eq8(juniorIdentities.emailVerified, true),
1735
+ or2(
1736
+ ...[...selectors.values()].map(
1737
+ (selector) => and7(
1738
+ eq8(juniorIdentities.providerTenantId, selector.teamId),
1739
+ eq8(juniorIdentities.providerSubjectId, selector.slackUserId)
1740
+ )
1741
+ )
1742
+ )
1861
1743
  )
1862
- };
1863
- }
1864
- function conversationActiveDaysColumn() {
1865
- return sql5`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
1866
- }
1867
-
1868
- // src/api/conversations/stats.query.ts
1869
- var WINDOW_DAYS = 90;
1870
- var treeConversation = alias4(juniorConversations, "stats_tree_conversation");
1871
- var treeAggregateColumns = conversationAggregateColumns({
1872
- metrics: treeConversation,
1873
- roots: juniorConversations
1874
- });
1875
- function emptyStatsItem(label) {
1876
- return {
1877
- active: 0,
1878
- conversations: 0,
1879
- durationMs: 0,
1880
- failed: 0,
1881
- label
1882
- };
1883
- }
1884
- function addUsd(current, next) {
1885
- return Math.round(((current ?? 0) + next) * 1e12) / 1e12;
1886
- }
1887
- function actorLabel(row) {
1888
- return row.userEmail?.trim() || row.identityEmail?.trim() || row.userDisplayName?.trim() || row.identityDisplayName?.trim() || row.identityHandle?.trim() || row.identitySubjectId?.trim() || "Unknown";
1744
+ );
1745
+ return new Map(
1746
+ rows.map((row) => [creatorKey(row.teamId, row.slackUserId), row.email])
1747
+ );
1889
1748
  }
1890
- function surfaceLabel(source) {
1891
- if (source === "scheduler") return "Scheduler";
1892
- if (source === "api") return "API";
1893
- if (source === "internal" || source === "local") return "Internal";
1894
- return "Conversation";
1749
+ function destinationKey(destination) {
1750
+ return `${destination.teamId}:${destination.channelId}`;
1895
1751
  }
1896
- function locationLabel(row) {
1897
- if (row.destinationProvider !== "slack") {
1898
- return surfaceLabel(row.source);
1899
- }
1900
- if (row.destinationKind === "dm") {
1901
- return "Direct Message";
1902
- }
1903
- if (row.destinationVisibility !== "public") {
1904
- return "Private Conversation";
1905
- }
1906
- const name = (row.channelName ?? row.destinationDisplayName)?.trim().replace(/^#/, "");
1907
- return name ? `#${name}` : "Public Channel";
1752
+ function displayText(value, fallback) {
1753
+ return value.trim() || fallback;
1908
1754
  }
1909
- function addAggregate(map, label, row) {
1910
- const item = map.get(label) ?? emptyStatsItem(label);
1911
- item.active += row.active;
1912
- item.conversations += row.conversations;
1913
- item.durationMs += row.durationMs;
1914
- item.failed += row.failed;
1915
- if (row.tokens !== null) {
1916
- item.tokens = (item.tokens ?? 0) + row.tokens;
1917
- }
1918
- if (row.costUsd !== null) {
1919
- item.costUsd = addUsd(item.costUsd, row.costUsd);
1920
- }
1921
- map.set(label, item);
1755
+ function taskDisplayTitle(title, instruction, fallback) {
1756
+ const stored = title?.trim();
1757
+ if (stored) return stored;
1758
+ return fallbackShortTitle(instruction, fallback);
1922
1759
  }
1923
- function statsItems(map) {
1924
- return [...map.values()].sort(
1925
- (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
1760
+ async function destinationDetails(destinations) {
1761
+ const selectors = new Map(
1762
+ destinations.map((destination) => [
1763
+ destinationKey(destination),
1764
+ destination
1765
+ ])
1926
1766
  );
1927
- }
1928
- function statsWhere(start, end) {
1929
- return and7(
1930
- isNull4(juniorConversations.parentConversationId),
1931
- gte(juniorConversations.lastActivityAt, start),
1932
- lte(juniorConversations.lastActivityAt, end)
1767
+ if (selectors.size === 0) return /* @__PURE__ */ new Map();
1768
+ const rows = await getDb().select({
1769
+ displayName: juniorDestinations.displayName,
1770
+ kind: juniorDestinations.kind,
1771
+ providerDestinationId: juniorDestinations.providerDestinationId,
1772
+ providerTenantId: juniorDestinations.providerTenantId,
1773
+ visibility: juniorDestinations.visibility
1774
+ }).from(juniorDestinations).where(
1775
+ or2(
1776
+ ...[...selectors.values()].map(
1777
+ (destination) => and7(
1778
+ eq8(juniorDestinations.provider, "slack"),
1779
+ eq8(juniorDestinations.providerTenantId, destination.teamId),
1780
+ eq8(juniorDestinations.providerDestinationId, destination.channelId)
1781
+ )
1782
+ )
1783
+ )
1784
+ );
1785
+ return new Map(
1786
+ rows.map((row) => {
1787
+ const name = row.displayName?.trim();
1788
+ const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
1789
+ return [
1790
+ `${row.providerTenantId}:${row.providerDestinationId}`,
1791
+ {
1792
+ label,
1793
+ visibility: row.visibility === "public" ? "public" : "private"
1794
+ }
1795
+ ];
1796
+ })
1933
1797
  );
1934
1798
  }
1935
- function statsWindow(nowMs) {
1936
- const end = new Date(nowMs);
1937
- const start = new Date(nowMs);
1938
- start.setUTCHours(0, 0, 0, 0);
1939
- start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
1940
- return { end, start };
1799
+ function executionSummaryFields(stats) {
1800
+ return {
1801
+ ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : {},
1802
+ ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : {},
1803
+ runsLast7Days: stats?.runsLast7Days ?? 0,
1804
+ totalRuns: stats?.totalRuns ?? 0
1805
+ };
1941
1806
  }
1942
- function metricDays(rows, endMs) {
1943
- const byDate = new Map(rows.map((row) => [row.date, row]));
1944
- const end = new Date(endMs);
1945
- end.setUTCHours(0, 0, 0, 0);
1946
- const start = new Date(end);
1947
- start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
1948
- const days = [];
1949
- for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
1950
- const date = cursor.toISOString().slice(0, 10);
1951
- const row = byDate.get(date);
1952
- days.push({
1953
- conversations: row?.conversations ?? 0,
1954
- date,
1955
- durationMs: row?.durationMs ?? 0,
1956
- ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: addUsd(void 0, row.costUsd) } : {},
1957
- ...row?.tokens !== null && row?.tokens !== void 0 ? { tokens: row.tokens } : {}
1958
- });
1807
+ function scheduledTaskSummary(task, ownedByViewer, destination, stats, createdByEmail) {
1808
+ if (task.status === "deleted") {
1809
+ throw new Error("Deleted scheduled tasks cannot enter the Tasks view");
1959
1810
  }
1960
- return days;
1811
+ const nextRunAtMs = task.runNowAtMs ?? task.nextRunAtMs;
1812
+ const instruction = displayText(task.task.text, "Untitled scheduled task");
1813
+ return {
1814
+ createdAt: new Date(task.createdAtMs).toISOString(),
1815
+ createdBy: creatorLabel(task.createdBy),
1816
+ ...createdByEmail ? { createdByEmail } : {},
1817
+ destination: {
1818
+ channelId: task.destination.channelId,
1819
+ label: destination.label,
1820
+ teamId: task.destination.teamId,
1821
+ visibility: destination.visibility
1822
+ },
1823
+ id: task.id,
1824
+ instruction,
1825
+ kind: "scheduled",
1826
+ ...executionSummaryFields(stats),
1827
+ ...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : {},
1828
+ ownedByViewer,
1829
+ schedule: displayText(task.schedule.description, "Schedule unavailable"),
1830
+ status: task.status,
1831
+ title: taskDisplayTitle(task.title, instruction, "Untitled scheduled task")
1832
+ };
1961
1833
  }
1962
- function guardianStats(rows, endMs) {
1963
- const byDate = new Map(rows.map((row) => [row.date, row]));
1964
- const end = new Date(endMs);
1965
- end.setUTCHours(0, 0, 0, 0);
1966
- const start = new Date(end);
1967
- start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
1968
- const metricDays2 = [];
1969
- let allow = 0;
1970
- let ask = 0;
1971
- let deny = 0;
1972
- let requests = 0;
1973
- let costUsd;
1974
- for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
1975
- const date = cursor.toISOString().slice(0, 10);
1976
- const row = byDate.get(date);
1977
- allow += row?.allow ?? 0;
1978
- ask += row?.ask ?? 0;
1979
- deny += row?.deny ?? 0;
1980
- requests += row?.requests ?? 0;
1981
- if (row?.costUsd !== null && row?.costUsd !== void 0) {
1982
- costUsd = addUsd(costUsd, row.costUsd);
1983
- }
1984
- metricDays2.push({
1985
- allow: row?.allow ?? 0,
1986
- ask: row?.ask ?? 0,
1987
- date,
1988
- deny: row?.deny ?? 0,
1989
- requests: row?.requests ?? 0,
1990
- ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: row.costUsd } : {}
1991
- });
1992
- }
1834
+ function eventTaskSummary(task, ownedByViewer, destination, stats, createdByEmail) {
1835
+ const instruction = task.task.text;
1993
1836
  return {
1994
- allow,
1995
- ask,
1996
- deny,
1997
- metricDays: metricDays2,
1998
- requests,
1999
- ...costUsd !== void 0 ? { costUsd } : {}
1837
+ createdAt: new Date(task.createdAtMs).toISOString(),
1838
+ createdBy: creatorLabel(task.createdBy),
1839
+ ...createdByEmail ? { createdByEmail } : {},
1840
+ destination: {
1841
+ channelId: task.destination.channelId,
1842
+ label: destination.label,
1843
+ teamId: task.destination.teamId,
1844
+ visibility: destination.visibility
1845
+ },
1846
+ events: task.trigger.events,
1847
+ id: task.id,
1848
+ instruction,
1849
+ kind: "event",
1850
+ ...executionSummaryFields(stats),
1851
+ ownedByViewer,
1852
+ resource: `${task.trigger.label} \xB7 ${task.trigger.identifier}`,
1853
+ source: task.trigger.namespace,
1854
+ title: taskDisplayTitle(task.title, instruction, "Untitled event task"),
1855
+ triggerAvailable: eventTaskTriggerAvailable(
1856
+ task,
1857
+ getResourceEventCatalog()
1858
+ )
2000
1859
  };
2001
1860
  }
2002
- async function aggregateStats(db, start, end) {
2003
- const where = statsWhere(start, end);
2004
- const activityDate = sql6`TO_CHAR(
2005
- ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2006
- 'YYYY-MM-DD'
2007
- )`;
2008
- const [totalsRows, actorRows, locationRows, metricRows] = await Promise.all([
2009
- db.select(treeAggregateColumns).from(juniorConversations).innerJoin(
2010
- treeConversation,
2011
- eq8(
2012
- treeConversation.rootConversationId,
2013
- juniorConversations.conversationId
2014
- )
2015
- ).where(where),
2016
- db.select({
2017
- identityDisplayName: juniorIdentities.displayName,
2018
- identityEmail: juniorIdentities.emailNormalized,
2019
- identityHandle: juniorIdentities.handle,
2020
- identitySubjectId: juniorIdentities.providerSubjectId,
2021
- userDisplayName: juniorUsers.displayName,
2022
- userEmail: juniorUsers.primaryEmailNormalized,
2023
- ...treeAggregateColumns
2024
- }).from(juniorConversations).innerJoin(
2025
- treeConversation,
2026
- eq8(
2027
- treeConversation.rootConversationId,
2028
- juniorConversations.conversationId
2029
- )
2030
- ).leftJoin(
2031
- juniorIdentities,
2032
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
2033
- ).leftJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
2034
- juniorIdentities.displayName,
2035
- juniorIdentities.emailNormalized,
2036
- juniorIdentities.handle,
2037
- juniorIdentities.providerSubjectId,
2038
- juniorUsers.displayName,
2039
- juniorUsers.primaryEmailNormalized
2040
- ),
2041
- db.select({
2042
- channelName: juniorConversations.channelName,
2043
- destinationDisplayName: juniorDestinations.displayName,
2044
- destinationKind: juniorDestinations.kind,
2045
- destinationProvider: juniorDestinations.provider,
2046
- destinationVisibility: juniorDestinations.visibility,
2047
- source: juniorConversations.source,
2048
- ...treeAggregateColumns
2049
- }).from(juniorConversations).innerJoin(
2050
- treeConversation,
2051
- eq8(
2052
- treeConversation.rootConversationId,
2053
- juniorConversations.conversationId
2054
- )
2055
- ).leftJoin(
2056
- juniorDestinations,
2057
- eq8(juniorDestinations.id, juniorConversations.destinationId)
2058
- ).where(where).groupBy(
2059
- juniorConversations.channelName,
2060
- juniorConversations.source,
2061
- juniorDestinations.displayName,
2062
- juniorDestinations.kind,
2063
- juniorDestinations.provider,
2064
- juniorDestinations.visibility
2065
- ),
2066
- db.select({
2067
- conversations: treeAggregateColumns.conversations,
2068
- costUsd: treeAggregateColumns.costUsd,
2069
- date: activityDate,
2070
- durationMs: treeAggregateColumns.durationMs,
2071
- tokens: treeAggregateColumns.tokens
2072
- }).from(juniorConversations).innerJoin(
2073
- treeConversation,
2074
- eq8(
2075
- treeConversation.rootConversationId,
2076
- juniorConversations.conversationId
2077
- )
2078
- ).where(where).groupBy(activityDate)
2079
- ]);
2080
- return { actorRows, locationRows, metricRows, totals: totalsRows[0] };
2081
- }
2082
- async function aggregateGuardianStats(db, start, end) {
2083
- const date = sql6`TO_CHAR(
2084
- ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
2085
- 'YYYY-MM-DD'
2086
- )`;
2087
- const decision = sql6`${juniorConversationEvents.payload}->>'decision'`;
2088
- const cost2 = sql6`CASE
2089
- WHEN jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
2090
- THEN (${juniorConversationEvents.payload}->>'costUsd')::double precision
2091
- ELSE NULL
2092
- END`;
2093
- return await db.select({
2094
- allow: sql6`COUNT(*) FILTER (WHERE ${decision} = 'allow')::integer`,
2095
- ask: sql6`COUNT(*) FILTER (WHERE ${decision} = 'ask')::integer`,
2096
- costUsd: sql6`SUM(${cost2})::double precision`,
2097
- date,
2098
- deny: sql6`COUNT(*) FILTER (WHERE ${decision} = 'deny')::integer`,
2099
- requests: sql6`COUNT(*)::integer`
2100
- }).from(juniorConversationEvents).where(
2101
- and7(
2102
- eq8(juniorConversationEvents.type, "guardian_action_reviewed"),
2103
- gte(juniorConversationEvents.createdAt, start),
2104
- lte(juniorConversationEvents.createdAt, end)
1861
+ function viewerTeamIds(user) {
1862
+ return [
1863
+ ...new Set(
1864
+ user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId))
2105
1865
  )
2106
- ).groupBy(date);
1866
+ ];
2107
1867
  }
2108
- async function readConversationStatsFromSql() {
2109
- const nowMs = Date.now();
2110
- const { end, start } = statsWindow(nowMs);
1868
+ async function resolveViewerTaskCandidate(user, kind, id) {
2111
1869
  const db = getDb();
2112
- const [{ actorRows, locationRows, metricRows, totals }, guardianRows] = await Promise.all([
2113
- aggregateStats(db, start, end),
2114
- aggregateGuardianStats(db, start, end)
2115
- ]);
2116
- const actors = /* @__PURE__ */ new Map();
2117
- const locations = /* @__PURE__ */ new Map();
2118
- for (const row of actorRows) {
2119
- addAggregate(actors, actorLabel(row), row);
2120
- }
2121
- for (const row of locationRows) {
2122
- addAggregate(locations, locationLabel(row), row);
1870
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
1871
+ const teamIds = viewerTeamIds(user);
1872
+ if (kind === "scheduled") {
1873
+ const task2 = await createSchedulerSqlStore(db).getTask(id);
1874
+ if (!task2 || task2.status === "deleted") return void 0;
1875
+ const ownedByViewer2 = identityIds.has(task2.creatorIdentityId);
1876
+ const destinations2 = await destinationDetails([task2.destination]);
1877
+ const destination2 = destinations2.get(destinationKey(task2.destination));
1878
+ const publicToViewer2 = teamIds.includes(task2.destination.teamId) && destination2?.visibility === "public";
1879
+ if (!ownedByViewer2 && !publicToViewer2) return void 0;
1880
+ return {
1881
+ kind: "scheduled",
1882
+ ownedByViewer: ownedByViewer2,
1883
+ publicToViewer: publicToViewer2,
1884
+ task: task2
1885
+ };
2123
1886
  }
1887
+ const task = await getEventTask(db, id);
1888
+ if (!task) return void 0;
1889
+ const ownedByViewer = eventTaskBelongsToUser(task, user);
1890
+ const destinations = await destinationDetails([task.destination]);
1891
+ const destination = destinations.get(destinationKey(task.destination));
1892
+ const publicToViewer = teamIds.includes(task.destination.teamId) && destination?.visibility === "public";
1893
+ if (!ownedByViewer && !publicToViewer) return void 0;
2124
1894
  return {
2125
- active: totals?.active ?? 0,
2126
- conversations: totals?.conversations ?? 0,
2127
- durationMs: totals?.durationMs ?? 0,
2128
- failed: totals?.failed ?? 0,
2129
- generatedAt: new Date(nowMs).toISOString(),
2130
- guardian: guardianStats(guardianRows, nowMs),
2131
- metricDays: metricDays(metricRows, nowMs),
2132
- locations: statsItems(locations),
2133
- actors: statsItems(actors),
2134
- source: "conversation_index",
2135
- ...totals?.costUsd !== null && totals?.costUsd !== void 0 ? { costUsd: addUsd(void 0, totals.costUsd) } : {},
2136
- ...totals?.tokens !== null && totals?.tokens !== void 0 ? { tokens: totals.tokens } : {},
2137
- windowEnd: end.toISOString(),
2138
- windowStart: start.toISOString()
1895
+ kind: "event",
1896
+ ownedByViewer,
1897
+ publicToViewer,
1898
+ task
2139
1899
  };
2140
1900
  }
2141
-
2142
- // src/api/conversations/stats.ts
2143
- async function readConversationStats() {
2144
- return conversationStatsReportSchema.parse(
2145
- await readConversationStatsFromSql()
1901
+ async function taskSummaryForCandidate(candidate) {
1902
+ const [destinations, creatorEmails, stats] = await Promise.all([
1903
+ destinationDetails([candidate.task.destination]),
1904
+ creatorProfileEmails([candidate]),
1905
+ readTaskExecutionSummaries(candidate.kind, "junior")
1906
+ ]);
1907
+ const destination = destinations.get(
1908
+ destinationKey(candidate.task.destination)
1909
+ ) ?? {
1910
+ label: `Channel ${candidate.task.destination.channelId}`,
1911
+ visibility: "private"
1912
+ };
1913
+ const createdByEmail = creatorEmails.get(
1914
+ creatorKey(
1915
+ candidate.task.destination.teamId,
1916
+ candidate.task.createdBy.slackUserId
1917
+ )
1918
+ );
1919
+ if (candidate.kind === "scheduled") {
1920
+ return scheduledTaskSummary(
1921
+ candidate.task,
1922
+ candidate.ownedByViewer,
1923
+ destination,
1924
+ stats.get(candidate.task.id),
1925
+ createdByEmail
1926
+ );
1927
+ }
1928
+ return eventTaskSummary(
1929
+ candidate.task,
1930
+ candidate.ownedByViewer,
1931
+ destination,
1932
+ stats.get(candidate.task.id),
1933
+ createdByEmail
2146
1934
  );
2147
1935
  }
2148
- var stats_default = defineApiRoute({
2149
- method: "get",
2150
- path: "/stats",
2151
- responseSchema: conversationStatsReportSchema,
2152
- handler: readConversationStats
2153
- });
2154
-
2155
- // src/api/conversations/routes.ts
2156
- var routes = [
2157
- list_default,
2158
- stats_default,
2159
- archive_default,
2160
- event_list_default,
2161
- detail_default
2162
- ];
2163
- function createConversationRoutes() {
2164
- const app = new Hono();
2165
- registerApiRoutes(app, routes);
2166
- return app;
2167
- }
2168
-
2169
- // src/reporting.ts
2170
- import { readFileSync } from "fs";
2171
- import path from "path";
2172
- function readDescriptionText(home) {
2173
- try {
2174
- const raw = readFileSync(path.join(home, "DESCRIPTION.md"), "utf8").trim();
2175
- return raw || void 0;
2176
- } catch {
2177
- return void 0;
1936
+ async function readViewerTasks(user) {
1937
+ const db = getDb();
1938
+ const schedulerStore = createSchedulerSqlStore(db);
1939
+ const identityIds = new Set(user.identities.map((identity) => identity.id));
1940
+ const teamIds = viewerTeamIds(user);
1941
+ const [scheduledPage, publicScheduled, eventTasks, publicEventTasks] = await Promise.all([
1942
+ createViewerScheduledTasks(schedulerStore, user).list({
1943
+ limit: TASK_FETCH_LIMIT
1944
+ }),
1945
+ listPublicScheduledTasksForTeams(db, teamIds, TASK_FETCH_LIMIT),
1946
+ listEventTasksCreatedBy(db, user, TASK_FETCH_LIMIT),
1947
+ listPublicEventTasksForTeams(db, teamIds, TASK_FETCH_LIMIT)
1948
+ ]);
1949
+ const candidatesById = /* @__PURE__ */ new Map();
1950
+ const publicScheduledIds = new Set(publicScheduled.map((task) => task.id));
1951
+ const publicEventTaskIds = new Set(publicEventTasks.map((task) => task.id));
1952
+ for (const task of [...scheduledPage.tasks, ...publicScheduled]) {
1953
+ candidatesById.set(`scheduled:${task.id}`, {
1954
+ kind: "scheduled",
1955
+ ownedByViewer: identityIds.has(task.creatorIdentityId),
1956
+ publicToViewer: publicScheduledIds.has(task.id),
1957
+ task
1958
+ });
2178
1959
  }
2179
- }
2180
- async function readSkillReports() {
2181
- const { discoverSkills } = await import("./skills-MJWRME3A.js");
2182
- const skills = await discoverSkills();
2183
- return skillReportsSchema.parse(
2184
- skills.map((skill) => ({
2185
- name: skill.name,
2186
- pluginProvider: skill.pluginProvider
2187
- }))
1960
+ for (const task of [...eventTasks, ...publicEventTasks]) {
1961
+ candidatesById.set(`event:${task.id}`, {
1962
+ kind: "event",
1963
+ ownedByViewer: eventTaskBelongsToUser(task, user),
1964
+ publicToViewer: publicEventTaskIds.has(task.id),
1965
+ task
1966
+ });
1967
+ }
1968
+ const candidates = [...candidatesById.values()].sort(
1969
+ (left, right) => right.task.createdAtMs - left.task.createdAtMs || right.task.id.localeCompare(left.task.id)
2188
1970
  );
2189
- }
2190
- async function readPlugins() {
2191
- const { pluginCatalogRuntime } = await import("./catalog-runtime-IAVW4W3B.js");
2192
- return pluginsSchema.parse(
2193
- pluginCatalogRuntime.getProviders().map((plugin) => ({
2194
- configKeys: plugin.manifest.configKeys ?? [],
2195
- description: plugin.manifest.description,
2196
- displayName: plugin.manifest.displayName,
2197
- name: plugin.manifest.name
2198
- }))
1971
+ const ownedCandidates = candidates.filter(
1972
+ (candidate) => candidate.ownedByViewer
2199
1973
  );
2200
- }
2201
- async function readRuntimeInfoReport() {
2202
- const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
2203
- import("./discovery-GUFTFBTL.js"),
2204
- import("./catalog-runtime-IAVW4W3B.js"),
2205
- readPlugins(),
2206
- readSkillReports()
1974
+ const publicCandidates = candidates.filter(
1975
+ (candidate) => candidate.publicToViewer
1976
+ );
1977
+ const selectedCandidates = /* @__PURE__ */ new Set([
1978
+ ...ownedCandidates.slice(0, TASK_LIST_LIMIT),
1979
+ ...publicCandidates.slice(0, TASK_LIST_LIMIT)
2207
1980
  ]);
2208
- const home = homeDir();
2209
- return runtimeInfoReportSchema.parse({
2210
- cwd: process.cwd(),
2211
- homeDir: home,
2212
- descriptionText: readDescriptionText(home),
2213
- providers: plugins.map((plugin) => plugin.name),
2214
- skills,
2215
- packagedContent: pluginCatalogRuntime.getPackageContent()
2216
- });
2217
- }
2218
- async function readPluginOperationalReportFeed() {
2219
- const nowMs = Date.now();
2220
- const { getPluginOperationalReports } = await import("./agent-hooks-2EP3QS47.js");
2221
- return pluginOperationalReportFeedSchema.parse({
2222
- source: "plugins",
2223
- generatedAt: new Date(nowMs).toISOString(),
2224
- reports: await getPluginOperationalReports(nowMs)
1981
+ const selected = candidates.filter(
1982
+ (candidate) => selectedCandidates.has(candidate)
1983
+ );
1984
+ const [destinations, creatorEmails] = await Promise.all([
1985
+ destinationDetails(selected.map(({ task }) => task.destination)),
1986
+ creatorProfileEmails(selected)
1987
+ ]);
1988
+ const visible = selected.filter(
1989
+ (candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public"
1990
+ );
1991
+ const [executionDays, scheduledStats, eventStats] = await Promise.all([
1992
+ readTaskExecutionDays(),
1993
+ readTaskExecutionSummaries("scheduled", "junior"),
1994
+ readTaskExecutionSummaries("event", "junior")
1995
+ ]);
1996
+ const tasks = visible.map((candidate) => {
1997
+ const destination = destinations.get(
1998
+ destinationKey(candidate.task.destination)
1999
+ ) ?? {
2000
+ label: `Channel ${candidate.task.destination.channelId}`,
2001
+ visibility: "private"
2002
+ };
2003
+ const createdByEmail = creatorEmails.get(
2004
+ creatorKey(
2005
+ candidate.task.destination.teamId,
2006
+ candidate.task.createdBy.slackUserId
2007
+ )
2008
+ );
2009
+ if (candidate.kind === "scheduled") {
2010
+ return scheduledTaskSummary(
2011
+ candidate.task,
2012
+ candidate.ownedByViewer,
2013
+ destination,
2014
+ scheduledStats.get(candidate.task.id),
2015
+ createdByEmail
2016
+ );
2017
+ }
2018
+ return eventTaskSummary(
2019
+ candidate.task,
2020
+ candidate.ownedByViewer,
2021
+ destination,
2022
+ eventStats.get(candidate.task.id),
2023
+ createdByEmail
2024
+ );
2225
2025
  });
2226
- }
2227
-
2228
- // src/api/health.ts
2229
- var health_default = defineApiRoute({
2230
- method: "get",
2231
- path: "/api/health",
2232
- responseSchema: healthReportSchema,
2233
- handler: readHealthReport
2234
- });
2235
-
2236
- // src/api/locations/routes.ts
2237
- import { Hono as Hono2 } from "hono";
2238
-
2239
- // src/api/locations/query.ts
2240
- import { and as and8, asc as asc3, desc as desc3, eq as eq9, gte as gte2, isNull as isNull5, sql as sql7 } from "drizzle-orm";
2241
- import { alias as alias5 } from "drizzle-orm/pg-core";
2242
-
2243
- // src/api/conversations/reporting.ts
2244
- function surfaceLabel2(surface) {
2245
- if (surface === "scheduler") return "Scheduler";
2246
- if (surface === "api") return "API";
2247
- if (surface === "internal") return "Internal";
2248
- return "Conversation";
2249
- }
2250
- function slackLocationLabel(args) {
2251
- const channelId = args.channel;
2252
- if (!channelId) return void 0;
2253
- if (args.channelNameRedacted && args.channelName) return args.channelName;
2254
- const name = args.channelName?.replace(/^#/, "");
2255
- if (channelId.startsWith("D")) return "Direct Message";
2256
- if (channelId.startsWith("C")) {
2257
- return name ? `#${name}` : "Public Channel";
2258
- }
2259
- if (channelId.startsWith("G")) {
2260
- if (name?.startsWith("mpdm-")) return "Group DM";
2261
- return "Private Channel";
2262
- }
2263
- return name || channelId;
2264
- }
2265
- function actorFromRow(row) {
2266
- const actor = {
2267
- ...row.email ? { email: row.email } : {},
2268
- ...row.fullName ? { fullName: row.fullName } : {},
2269
- ...row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
2270
- ...row.handle ? { slackUserName: row.handle } : {}
2026
+ return {
2027
+ executionDays,
2028
+ tasks,
2029
+ truncated: ownedCandidates.length > TASK_LIST_LIMIT || publicCandidates.length > TASK_LIST_LIMIT
2271
2030
  };
2272
- return Object.keys(actor).length ? actor : void 0;
2273
2031
  }
2274
- function summaryFromRow(row, options = {}) {
2275
- const actor = actorFromRow(row);
2276
- const conversation = {
2277
- conversationId: row.conversationId,
2278
- createdAtMs: row.createdAt.getTime(),
2279
- lastActivityAtMs: row.lastActivityAt.getTime(),
2280
- updatedAtMs: row.updatedAt.getTime(),
2281
- execution: {
2282
- status: row.executionStatus,
2283
- ...row.executionUpdatedAt ? { updatedAtMs: row.executionUpdatedAt.getTime() } : {}
2284
- },
2285
- ...actor ? { actor } : {},
2286
- ...row.channelName ? { channelName: row.channelName } : {},
2287
- ...row.source ? { source: row.source } : {},
2288
- ...row.title ? { title: row.title } : {}
2289
- };
2290
- return conversationSummaryFromStoredConversation({
2291
- access: options.access,
2292
- conversation,
2293
- durationMs: options.metrics?.durationMs ?? row.durationMs,
2294
- ...options.access?.visibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
2295
- usage: options.metrics?.usage ?? row.usage ?? void 0
2032
+ async function readViewerTaskRuns(user) {
2033
+ const taskList = await readViewerTasks(user);
2034
+ const taskTitles = new Map(
2035
+ taskList.tasks.map((task) => [`${task.kind}:${task.id}`, task.title])
2036
+ );
2037
+ const runs = await readTaskRuns({
2038
+ limit: TASK_EXECUTION_LIST_LIMIT + 1,
2039
+ tasks: taskList.tasks.map((task) => ({
2040
+ kind: task.kind,
2041
+ taskId: task.id
2042
+ }))
2296
2043
  });
2297
- }
2298
-
2299
- // src/api/locations/query.ts
2300
- var RECENT_LIMIT = 25;
2301
- var ACTIVITY_DAYS = 90;
2302
- var treeConversation2 = alias5(
2303
- juniorConversations,
2304
- "location_tree_conversation"
2305
- );
2306
- var treeAggregateColumns2 = conversationAggregateColumns({
2307
- metrics: treeConversation2,
2308
- roots: juniorConversations
2309
- });
2310
- function emptyMetrics() {
2311
2044
  return {
2312
- active: 0,
2313
- conversations: 0,
2314
- durationMs: 0,
2315
- failed: 0
2045
+ runs: runs.slice(0, TASK_EXECUTION_LIST_LIMIT).map((run) => ({
2046
+ ...run,
2047
+ taskTitle: taskTitles.get(`${run.kind}:${run.taskId}`) ?? "Untitled task"
2048
+ })),
2049
+ truncated: runs.length > TASK_EXECUTION_LIST_LIMIT
2316
2050
  };
2317
2051
  }
2318
- function addMetrics(target, row) {
2319
- target.active += row.active;
2320
- target.conversations += row.conversations;
2321
- target.durationMs += row.durationMs;
2322
- target.failed += row.failed;
2323
- if (row.tokens !== null) {
2324
- target.tokens = (target.tokens ?? 0) + row.tokens;
2325
- }
2326
- if (row.costUsd !== null) {
2327
- target.costUsd = Math.round(((target.costUsd ?? 0) + row.costUsd) * 1e12) / 1e12;
2052
+ var ViewerTaskNotFoundError = class extends Error {
2053
+ constructor() {
2054
+ super("Task was not found.");
2055
+ this.name = "ViewerTaskNotFoundError";
2328
2056
  }
2329
- }
2330
- function publicLabel(input) {
2331
- const name = input.displayName?.trim().replace(/^#/, "");
2332
- return name ? `#${name}` : `Public channel ${input.providerDestinationId}`;
2333
- }
2334
- function actorLabel2(actor) {
2335
- return actor.email?.trim() || actor.fullName?.trim() || actor.slackUserName?.trim() || actor.slackUserId?.trim() || "Unknown";
2336
- }
2337
- function emptyActor(actor) {
2057
+ };
2058
+ async function readViewerTaskExecutions(user, kind, id) {
2059
+ const candidate = await resolveViewerTaskCandidate(user, kind, id);
2060
+ if (!candidate) throw new ViewerTaskNotFoundError();
2061
+ const [task, executions, executionDays] = await Promise.all([
2062
+ taskSummaryForCandidate(candidate),
2063
+ readTaskExecutions({
2064
+ kind,
2065
+ limit: TASK_EXECUTION_LIST_LIMIT + 1,
2066
+ taskId: id
2067
+ }),
2068
+ readTaskExecutionStatusDays({
2069
+ kind,
2070
+ taskId: id
2071
+ })
2072
+ ]);
2338
2073
  return {
2339
- ...emptyMetrics(),
2340
- actor,
2341
- label: actorLabel2(actor)
2074
+ executionDays,
2075
+ executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
2076
+ task,
2077
+ truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
2342
2078
  };
2343
2079
  }
2344
- function topLevelWhere() {
2345
- return isNull5(juniorConversations.parentConversationId);
2346
- }
2347
- function publicLocationWhere(destinationId) {
2348
- return and8(
2349
- topLevelWhere(),
2350
- eq9(juniorDestinations.id, destinationId),
2351
- eq9(juniorDestinations.visibility, "public")
2080
+ async function readConversationSourceTask(args) {
2081
+ const execution = await readTaskExecutionByConversationId({
2082
+ conversationId: args.conversationId
2083
+ });
2084
+ if (!execution) return void 0;
2085
+ const email = args.verifiedViewerEmail?.trim();
2086
+ if (!email) return { kind: execution.kind };
2087
+ const user = await resolveViewerUser(email);
2088
+ if (!user) return { kind: execution.kind };
2089
+ const candidate = await resolveViewerTaskCandidate(
2090
+ user,
2091
+ execution.kind,
2092
+ execution.taskId
2093
+ );
2094
+ if (!candidate) return { kind: execution.kind };
2095
+ if (candidate.kind === "scheduled") {
2096
+ const instruction2 = displayText(
2097
+ candidate.task.task.text,
2098
+ "Untitled scheduled task"
2099
+ );
2100
+ return {
2101
+ id: candidate.task.id,
2102
+ kind: "scheduled",
2103
+ label: instruction2,
2104
+ title: taskDisplayTitle(
2105
+ candidate.task.title,
2106
+ instruction2,
2107
+ "Untitled scheduled task"
2108
+ )
2109
+ };
2110
+ }
2111
+ const instruction = displayText(
2112
+ candidate.task.task.text,
2113
+ "Untitled event task"
2352
2114
  );
2353
- }
2354
- function locationColumns() {
2355
2115
  return {
2356
- destinationDisplayName: juniorDestinations.displayName,
2357
- destinationId: juniorDestinations.id,
2358
- destinationKind: juniorDestinations.kind,
2359
- destinationProvider: juniorDestinations.provider,
2360
- destinationProviderId: juniorDestinations.providerDestinationId,
2361
- destinationVisibility: juniorDestinations.visibility
2116
+ id: candidate.task.id,
2117
+ kind: "event",
2118
+ label: instruction,
2119
+ title: taskDisplayTitle(
2120
+ candidate.task.title,
2121
+ instruction,
2122
+ "Untitled event task"
2123
+ )
2362
2124
  };
2363
2125
  }
2364
- function locationGroupBy() {
2365
- return [
2366
- juniorDestinations.displayName,
2367
- juniorDestinations.id,
2368
- juniorDestinations.kind,
2369
- juniorDestinations.provider,
2370
- juniorDestinations.providerDestinationId,
2371
- juniorDestinations.visibility
2372
- ];
2373
- }
2374
- function locationFromAggregate(row) {
2375
- if (row.destinationVisibility !== "public" || !row.destinationId || !row.destinationProvider || !row.destinationProviderId || !row.destinationKind) {
2376
- return void 0;
2126
+ async function deleteViewerTask(user, kind, id) {
2127
+ if (kind === "scheduled") {
2128
+ try {
2129
+ await createViewerScheduledTasks(
2130
+ createSchedulerSqlStore(getDb()),
2131
+ user
2132
+ ).delete(id);
2133
+ return;
2134
+ } catch (error) {
2135
+ if (error instanceof PersonalScheduledTaskNotFoundError) {
2136
+ throw new ViewerTaskNotFoundError();
2137
+ }
2138
+ throw error;
2139
+ }
2377
2140
  }
2378
- const location = {
2379
- ...emptyMetrics(),
2380
- firstSeenAt: row.firstSeenAt.toISOString(),
2381
- id: row.destinationId,
2382
- kind: row.destinationKind,
2383
- label: publicLabel({
2384
- displayName: row.destinationDisplayName,
2385
- providerDestinationId: row.destinationProviderId
2141
+ const task = await getEventTask(getDb(), id);
2142
+ if (!task || !eventTaskBelongsToUser(task, user)) {
2143
+ throw new ViewerTaskNotFoundError();
2144
+ }
2145
+ await deleteEventTask(getDb(), id);
2146
+ }
2147
+
2148
+ // src/api/conversations/detail.ts
2149
+ function projectConversationDetail(args) {
2150
+ const { conversation } = args;
2151
+ const conversationId = conversation.conversationId;
2152
+ const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
2153
+ const canExposePayload = args.access?.canViewPrivateContent ?? false;
2154
+ const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
2155
+ const sentryConversationUrl = buildSentryConversationUrl(conversationId);
2156
+ return {
2157
+ ...conversationSummaryFromStoredConversation({
2158
+ access: args.access,
2159
+ auxiliaryCosts: args.auxiliaryCosts,
2160
+ conversation,
2161
+ durationMs: args.durationMs,
2162
+ ...args.locationId ? { locationId: args.locationId } : {},
2163
+ ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : {},
2164
+ usage: args.usage
2386
2165
  }),
2387
- lastSeenAt: row.lastSeenAt.toISOString(),
2388
- provider: row.destinationProvider,
2389
- providerDestinationId: row.destinationProviderId,
2390
- visibility: "public"
2166
+ annotations: canExposePayload ? args.annotations : [],
2167
+ events: args.events,
2168
+ ...args.previousSeq !== void 0 ? {
2169
+ previousCursor: encodeConversationCursor({
2170
+ conversationId,
2171
+ seq: args.previousSeq
2172
+ })
2173
+ } : {},
2174
+ ...modelUsage.length > 0 ? { modelUsage } : {},
2175
+ eventHistory: conversationEventHistory({
2176
+ canExposePayload,
2177
+ ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
2178
+ }),
2179
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2180
+ ...sentryConversationUrl ? { sentryConversationUrl } : {},
2181
+ ...args.sourceTask ? { sourceTask: args.sourceTask } : {}
2391
2182
  };
2392
- addMetrics(location, row);
2393
- return location;
2394
2183
  }
2395
- async function directoryRows(db) {
2396
- return db.select({
2397
- ...locationColumns(),
2398
- ...treeAggregateColumns2,
2399
- ...conversationRangeColumns()
2400
- }).from(juniorConversations).innerJoin(
2401
- treeConversation2,
2402
- eq9(
2403
- treeConversation2.rootConversationId,
2404
- juniorConversations.conversationId
2184
+ async function readConversationDetailFromSql(conversationId, options) {
2185
+ const record = await readConversationRecordFromSql(conversationId);
2186
+ if (!record) return void 0;
2187
+ const executor = getSqlExecutor();
2188
+ const includeDescendantMetrics = record.rootConversationId === conversationId;
2189
+ const [
2190
+ accessByConversation,
2191
+ annotations,
2192
+ auxiliaryCostsByConversation,
2193
+ modelUsage,
2194
+ metricsByRoot,
2195
+ sourceTask,
2196
+ teamDomainByTeamId
2197
+ ] = await Promise.all([
2198
+ readConversationAccessFromSql(
2199
+ getDb(),
2200
+ [conversationId],
2201
+ options.verifiedViewerEmail
2202
+ ),
2203
+ listConversationAnnotations(getDb(), conversationId),
2204
+ readConversationAuxiliaryCostsFromSql(getDb(), [conversationId], {
2205
+ includeDescendants: includeDescendantMetrics
2206
+ }),
2207
+ record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, {
2208
+ conversationId,
2209
+ includeDescendants: includeDescendantMetrics
2210
+ }) : Promise.resolve([]),
2211
+ readRootConversationMetricsFromSql(
2212
+ getDb(),
2213
+ includeDescendantMetrics ? [conversationId] : []
2214
+ ),
2215
+ readConversationSourceTask({
2216
+ conversationId,
2217
+ ...options.verifiedViewerEmail ? { verifiedViewerEmail: options.verifiedViewerEmail } : {}
2218
+ }),
2219
+ resolveSlackTeamDomains(
2220
+ record.conversation.sessionSource?.platform === "slack" ? [record.conversation.sessionSource.teamId] : []
2405
2221
  )
2406
- ).leftJoin(
2407
- juniorDestinations,
2408
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2409
- ).where(topLevelWhere()).groupBy(...locationGroupBy());
2222
+ ]);
2223
+ const access = accessByConversation.get(conversationId);
2224
+ const page = record.conversation.transcriptPurgedAtMs === void 0 ? await readConversationEventPage(executor, {
2225
+ canExposePayload: access?.canViewPrivateContent ?? false,
2226
+ conversationId,
2227
+ limit: options.limit
2228
+ }) : { events: [] };
2229
+ const metrics = metricsByRoot.get(conversationId);
2230
+ return projectConversationDetail({
2231
+ ...record,
2232
+ access,
2233
+ annotations,
2234
+ auxiliaryCosts: auxiliaryCostsByConversation.get(conversationId),
2235
+ durationMs: metrics?.durationMs ?? record.durationMs,
2236
+ events: page.events,
2237
+ modelUsage,
2238
+ ...sourceTask ? { sourceTask } : {},
2239
+ teamDomainByTeamId,
2240
+ ...page.previousSeq === void 0 ? {} : { previousSeq: page.previousSeq },
2241
+ usage: metrics?.usage ?? record.usage ?? void 0
2242
+ });
2410
2243
  }
2411
- async function directoryActivityRows(db, start) {
2412
- const date = sql7`TO_CHAR(
2413
- ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2414
- 'YYYY-MM-DD'
2415
- )`;
2416
- return db.select({
2417
- conversations: sql7`COUNT(*)::integer`,
2418
- date,
2419
- visibility: juniorDestinations.visibility
2420
- }).from(juniorConversations).leftJoin(
2421
- juniorDestinations,
2422
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2423
- ).where(and8(topLevelWhere(), gte2(juniorConversations.lastActivityAt, start))).groupBy(date, juniorDestinations.visibility);
2244
+ async function readConversationDetail(conversationId, options = {}) {
2245
+ const report = await readConversationDetailFromSql(conversationId, {
2246
+ ...options,
2247
+ limit: options.limit ?? 500
2248
+ });
2249
+ return report ? conversationDetailReportSchema.parse(report) : void 0;
2424
2250
  }
2425
- function directoryActivityDays(rows, nowMs) {
2426
- const days = /* @__PURE__ */ new Map();
2427
- for (const row of rows) {
2428
- const day = days.get(row.date) ?? {
2429
- date: row.date,
2430
- privateConversations: 0,
2431
- publicConversations: 0
2432
- };
2433
- if (row.visibility === "public") {
2434
- day.publicConversations += row.conversations;
2435
- } else {
2436
- day.privateConversations += row.conversations;
2437
- }
2438
- days.set(row.date, day);
2439
- }
2440
- const end = new Date(nowMs);
2441
- end.setUTCHours(0, 0, 0, 0);
2442
- const start = new Date(end);
2443
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2444
- const activity = [];
2445
- for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2446
- const date = cursor.toISOString().slice(0, 10);
2447
- activity.push(
2448
- days.get(date) ?? {
2449
- date,
2450
- privateConversations: 0,
2451
- publicConversations: 0
2452
- }
2251
+ var detail_default = defineApiRoute({
2252
+ method: "get",
2253
+ path: "/:conversationId",
2254
+ responseSchema: conversationDetailReportSchema,
2255
+ handler: async (c) => {
2256
+ const { conversationId } = parseParams(
2257
+ conversationParamsSchema,
2258
+ c.req.param()
2453
2259
  );
2454
- }
2455
- return activity;
2456
- }
2457
- async function readLocationDirectoryFromSql() {
2458
- const nowMs = Date.now();
2459
- const end = new Date(nowMs);
2460
- end.setUTCHours(0, 0, 0, 0);
2461
- const start = new Date(end);
2462
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2463
- const [rows, activityRows] = await Promise.all([
2464
- directoryRows(getDb()),
2465
- directoryActivityRows(getDb(), start)
2466
- ]);
2467
- const locations = [];
2468
- const privateActivity = {
2469
- ...emptyMetrics(),
2470
- label: "Private activity"
2471
- };
2472
- for (const row of rows) {
2473
- const location = locationFromAggregate(row);
2474
- if (location) locations.push(location);
2475
- else addMetrics(privateActivity, row);
2476
- }
2477
- return {
2478
- activityDays: directoryActivityDays(activityRows, nowMs),
2479
- generatedAt: new Date(nowMs).toISOString(),
2480
- locations: locations.sort(
2481
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.label.localeCompare(right.label)
2482
- ),
2483
- privateActivity,
2484
- source: "conversation_index",
2485
- windowEnd: end.toISOString(),
2486
- windowStart: start.toISOString()
2487
- };
2488
- }
2489
- async function recentLocationRows(db, locationId) {
2490
- return db.select({
2491
- channelName: juniorConversations.channelName,
2492
- conversationId: juniorConversations.conversationId,
2493
- createdAt: juniorConversations.createdAt,
2494
- destinationId: juniorDestinations.id,
2495
- durationMs: juniorConversations.durationMs,
2496
- email: sql7`COALESCE(
2497
- ${juniorUsers.primaryEmailNormalized},
2498
- ${juniorIdentities.email}
2499
- )`,
2500
- executionStatus: juniorConversations.executionStatus,
2501
- executionUpdatedAt: juniorConversations.executionUpdatedAt,
2502
- fullName: juniorUsers.displayName,
2503
- handle: juniorIdentities.handle,
2504
- lastActivityAt: juniorConversations.lastActivityAt,
2505
- providerSubjectId: sql7`CASE
2506
- WHEN ${juniorIdentities.provider} = 'slack'
2507
- THEN ${juniorIdentities.providerSubjectId}
2508
- ELSE NULL
2509
- END`,
2510
- source: juniorConversations.source,
2511
- title: juniorConversations.title,
2512
- updatedAt: juniorConversations.updatedAt,
2513
- usage: juniorConversations.usage
2514
- }).from(juniorConversations).innerJoin(
2515
- juniorDestinations,
2516
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2517
- ).leftJoin(
2518
- juniorIdentities,
2519
- eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
2520
- ).leftJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).where(
2521
- and8(
2522
- publicLocationWhere(locationId),
2523
- isNull5(juniorConversations.archivedAt)
2524
- )
2525
- ).orderBy(
2526
- desc3(juniorConversations.lastActivityAt),
2527
- asc3(juniorConversations.conversationId)
2528
- ).limit(RECENT_LIMIT);
2529
- }
2530
- async function readLocationDetailFromSql(locationId, options = {}) {
2531
- const nowMs = Date.now();
2532
- const end = new Date(nowMs);
2533
- end.setUTCHours(0, 0, 0, 0);
2534
- const start = new Date(end);
2535
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2536
- const where = publicLocationWhere(locationId);
2537
- const activityDate = sql7`TO_CHAR(
2538
- ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2539
- 'YYYY-MM-DD'
2540
- )`;
2541
- const [locationRows, dayRows, actorRows, recentRows] = await Promise.all([
2542
- getDb().select({
2543
- ...locationColumns(),
2544
- ...treeAggregateColumns2,
2545
- ...conversationRangeColumns()
2546
- }).from(juniorConversations).innerJoin(
2547
- treeConversation2,
2548
- eq9(
2549
- treeConversation2.rootConversationId,
2550
- juniorConversations.conversationId
2551
- )
2552
- ).innerJoin(
2553
- juniorDestinations,
2554
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2555
- ).where(where).groupBy(...locationGroupBy()),
2556
- getDb().select({
2557
- date: activityDate,
2558
- ...treeAggregateColumns2
2559
- }).from(juniorConversations).innerJoin(
2560
- treeConversation2,
2561
- eq9(
2562
- treeConversation2.rootConversationId,
2563
- juniorConversations.conversationId
2564
- )
2565
- ).innerJoin(
2566
- juniorDestinations,
2567
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2568
- ).where(and8(where, gte2(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
2569
- getDb().select({
2570
- actorIdentityId: juniorConversations.actorIdentityId,
2571
- email: juniorUsers.primaryEmailNormalized,
2572
- fullName: juniorUsers.displayName,
2573
- handle: juniorIdentities.handle,
2574
- identityEmail: juniorIdentities.email,
2575
- identityProvider: juniorIdentities.provider,
2576
- providerSubjectId: juniorIdentities.providerSubjectId,
2577
- ...treeAggregateColumns2
2578
- }).from(juniorConversations).innerJoin(
2579
- treeConversation2,
2580
- eq9(
2581
- treeConversation2.rootConversationId,
2582
- juniorConversations.conversationId
2583
- )
2584
- ).innerJoin(
2585
- juniorDestinations,
2586
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2587
- ).leftJoin(
2588
- juniorIdentities,
2589
- eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
2590
- ).leftJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
2591
- juniorConversations.actorIdentityId,
2592
- juniorUsers.primaryEmailNormalized,
2593
- juniorUsers.displayName,
2594
- juniorIdentities.handle,
2595
- juniorIdentities.email,
2596
- juniorIdentities.provider,
2597
- juniorIdentities.providerSubjectId
2598
- ),
2599
- recentLocationRows(getDb(), locationId)
2600
- ]);
2601
- const locationRow = locationRows[0];
2602
- const location = locationRow ? locationFromAggregate(locationRow) : void 0;
2603
- if (!location) return void 0;
2604
- const days = /* @__PURE__ */ new Map();
2605
- for (const row of dayRows) {
2606
- days.set(row.date, {
2607
- active: row.active,
2608
- conversations: row.conversations,
2609
- date: row.date,
2610
- durationMs: row.durationMs,
2611
- failed: row.failed,
2612
- ...row.tokens !== null ? { tokens: row.tokens } : {}
2260
+ const query = parseQuery(conversationDetailQuerySchema, c.req.query());
2261
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
2262
+ const report = await readConversationDetail(conversationId, {
2263
+ ...query,
2264
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
2613
2265
  });
2266
+ if (!report) throwApiError(404, "Conversation not found.");
2267
+ return report;
2614
2268
  }
2615
- const actors = [];
2616
- for (const row of actorRows) {
2617
- if (!row.actorIdentityId) continue;
2618
- const actor = {
2619
- ...row.email ?? row.identityEmail ? { email: row.email ?? row.identityEmail ?? void 0 } : {},
2620
- ...row.fullName ? { fullName: row.fullName } : {},
2621
- ...row.identityProvider === "slack" && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
2622
- ...row.handle ? { slackUserName: row.handle } : {}
2623
- };
2624
- if (!actor.email && !actor.slackUserId) continue;
2625
- const item = emptyActor(actor);
2626
- addMetrics(item, row);
2627
- actors.push(item);
2628
- }
2629
- const activity = activityDays(days, nowMs, ACTIVITY_DAYS);
2630
- const recentConversationIds = recentRows.map((row) => row.conversationId);
2631
- const [accessByConversation, metricsByRoot] = await Promise.all([
2632
- readConversationAccessFromSql(
2633
- getDb(),
2634
- recentConversationIds,
2635
- options.verifiedViewerEmail
2636
- ),
2637
- readRootConversationMetricsFromSql(getDb(), recentConversationIds)
2638
- ]);
2639
- return {
2640
- ...location,
2641
- activityDays: activity,
2642
- actors: actors.sort(
2643
- (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
2644
- ),
2645
- generatedAt: new Date(nowMs).toISOString(),
2646
- recentConversations: recentRows.map(
2647
- (row) => summaryFromRow(row, {
2648
- access: accessByConversation.get(row.conversationId),
2649
- metrics: metricsByRoot.get(row.conversationId)
2650
- })
2651
- ),
2652
- source: "conversation_index",
2653
- windowEnd: end.toISOString(),
2654
- windowStart: activity[0] ? `${activity[0].date}T00:00:00.000Z` : end.toISOString()
2655
- };
2656
- }
2269
+ });
2657
2270
 
2658
- // src/api/locations/detail.ts
2659
- async function readLocationDetail(locationId, options = {}) {
2660
- const report = await readLocationDetailFromSql(locationId, options);
2661
- return report ? locationDetailReportSchema.parse(report) : void 0;
2271
+ // src/api/conversations/event-list.ts
2272
+ async function readConversationEvents(conversationId, beforeValue, options = {}) {
2273
+ const record = await readConversationRecordFromSql(conversationId);
2274
+ if (!record) return void 0;
2275
+ const before = decodeConversationCursor({
2276
+ conversationId,
2277
+ cursor: beforeValue
2278
+ });
2279
+ if (!before) throwApiError(400, "Invalid conversation cursor.");
2280
+ const accessByConversation = await readConversationAccessFromSql(
2281
+ getDb(),
2282
+ [conversationId],
2283
+ options.verifiedViewerEmail
2284
+ );
2285
+ const access = accessByConversation.get(conversationId);
2286
+ const canExposePayload = access?.canViewPrivateContent ?? false;
2287
+ const transcriptPurgedAtMs = record.conversation.transcriptPurgedAtMs;
2288
+ const page = transcriptPurgedAtMs === void 0 ? await readConversationEventPage(getSqlExecutor(), {
2289
+ beforeSeq: before.seq,
2290
+ canExposePayload,
2291
+ conversationId,
2292
+ limit: options.limit ?? 500
2293
+ }) : { events: [] };
2294
+ return conversationEventPageSchema.parse({
2295
+ events: page.events,
2296
+ eventHistory: conversationEventHistory({
2297
+ canExposePayload,
2298
+ ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
2299
+ }),
2300
+ ...page.previousSeq === void 0 ? {} : {
2301
+ previousCursor: encodeConversationCursor({
2302
+ conversationId,
2303
+ seq: page.previousSeq
2304
+ })
2305
+ },
2306
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
2307
+ });
2662
2308
  }
2663
- var detail_default2 = defineApiRoute({
2309
+ var event_list_default = defineApiRoute({
2664
2310
  method: "get",
2665
- path: "/:locationId",
2666
- responseSchema: locationDetailReportSchema,
2311
+ path: "/:conversationId/events",
2312
+ responseSchema: conversationEventPageSchema,
2667
2313
  handler: async (c) => {
2668
- const { locationId } = parseParams(locationParamsSchema, c.req.param());
2669
- const verifiedViewerEmail = c.get("verifiedViewerEmail");
2670
- const report = await readLocationDetail(
2671
- locationId,
2672
- verifiedViewerEmail ? { verifiedViewerEmail } : {}
2314
+ const { conversationId } = parseParams(
2315
+ conversationParamsSchema,
2316
+ c.req.param()
2673
2317
  );
2674
- if (!report) throwApiError(404, "Location not found.");
2318
+ const { before, limit } = parseQuery(
2319
+ conversationEventsQuerySchema,
2320
+ c.req.query()
2321
+ );
2322
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
2323
+ const report = await readConversationEvents(conversationId, before, {
2324
+ limit,
2325
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
2326
+ });
2327
+ if (!report) throwApiError(404, "Conversation not found.");
2675
2328
  return report;
2676
2329
  }
2677
2330
  });
2678
2331
 
2679
- // src/api/locations/list.ts
2680
- async function readLocationDirectory() {
2681
- return locationDirectoryReportSchema.parse(
2682
- await readLocationDirectoryFromSql()
2683
- );
2684
- }
2685
- var list_default2 = defineApiRoute({
2686
- method: "get",
2687
- path: "/",
2688
- responseSchema: locationDirectoryReportSchema,
2689
- handler: readLocationDirectory
2690
- });
2332
+ // src/api/conversations/stats.query.ts
2333
+ import { and as and8, eq as eq9, gte, isNull as isNull4, lte, sql as sql6 } from "drizzle-orm";
2334
+ import { alias as alias4 } from "drizzle-orm/pg-core";
2691
2335
 
2692
- // src/api/locations/routes.ts
2693
- var routes2 = [list_default2, detail_default2];
2694
- function createLocationRoutes() {
2695
- const app = new Hono2();
2696
- registerApiRoutes(app, routes2);
2697
- return app;
2336
+ // src/api/conversations/aggregate.ts
2337
+ import { sql as sql5 } from "drizzle-orm";
2338
+ function tokenValue(source) {
2339
+ return sql5`
2340
+ CASE
2341
+ WHEN COALESCE(
2342
+ ${source.usage}->>'inputTokens',
2343
+ ${source.usage}->>'outputTokens',
2344
+ ${source.usage}->>'cachedInputTokens',
2345
+ ${source.usage}->>'cacheCreationTokens'
2346
+ ) IS NOT NULL
2347
+ THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
2348
+ + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
2349
+ + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
2350
+ + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
2351
+ WHEN ${source.usage}->>'totalTokens' IS NOT NULL
2352
+ THEN (${source.usage}->>'totalTokens')::double precision
2353
+ ELSE NULL
2354
+ END
2355
+ `;
2698
2356
  }
2699
-
2700
- // src/api/people/routes.ts
2701
- import { Hono as Hono3 } from "hono";
2702
-
2703
- // src/api/people/list.query.ts
2704
- import { and as and10, eq as eq11, gte as gte3, sql as sql9 } from "drizzle-orm";
2705
-
2706
- // src/api/people/shared.ts
2707
- import { and as and9, asc as asc4, desc as desc4, eq as eq10, isNull as isNull6, notExists, or as or2, sql as sql8 } from "drizzle-orm";
2708
- import { alias as alias6 } from "drizzle-orm/pg-core";
2709
- var RECENT_LIMIT2 = 25;
2710
- var ACTIVITY_DAYS2 = 365;
2711
- var peopleTreeConversation = alias6(
2712
- juniorConversations,
2713
- "people_tree_conversation"
2714
- );
2715
- var peopleRootConversation = alias6(
2716
- juniorConversations,
2717
- "people_root_conversation"
2718
- );
2719
- var peopleRootIdentity = alias6(juniorIdentities, "people_root_identity");
2720
- var peopleTreeAggregateColumns = conversationAggregateColumns({
2721
- metrics: peopleTreeConversation,
2722
- roots: juniorConversations
2723
- });
2724
- function peopleTreeMetricsJoin() {
2725
- const rootOwnedByActor = getDb().select({ conversationId: peopleRootConversation.conversationId }).from(peopleRootConversation).innerJoin(
2726
- peopleRootIdentity,
2727
- eq10(peopleRootIdentity.id, peopleRootConversation.actorIdentityId)
2728
- ).where(
2729
- and9(
2730
- eq10(
2731
- peopleRootConversation.conversationId,
2732
- juniorConversations.rootConversationId
2733
- ),
2734
- isNull6(peopleRootConversation.parentConversationId),
2735
- eq10(
2736
- peopleRootConversation.rootConversationId,
2737
- peopleRootConversation.conversationId
2738
- ),
2739
- eq10(
2740
- peopleRootIdentity.emailNormalized,
2741
- juniorUsers.primaryEmailNormalized
2742
- ),
2743
- eq10(peopleRootIdentity.emailVerified, true)
2744
- )
2745
- );
2746
- return and9(
2747
- eq10(
2748
- peopleTreeConversation.rootConversationId,
2749
- juniorConversations.rootConversationId
2750
- ),
2751
- or2(
2752
- eq10(
2753
- juniorConversations.rootConversationId,
2754
- juniorConversations.conversationId
2755
- ),
2756
- and9(
2757
- eq10(
2758
- peopleTreeConversation.conversationId,
2759
- juniorConversations.conversationId
2760
- ),
2761
- notExists(rootOwnedByActor)
2762
- )
2763
- )
2764
- );
2357
+ function costValue(source) {
2358
+ return sql5`
2359
+ CASE
2360
+ WHEN ${source.usage}->'cost'->>'total' IS NOT NULL
2361
+ THEN (${source.usage}->'cost'->>'total')::double precision
2362
+ WHEN COALESCE(
2363
+ ${source.usage}->'cost'->>'input',
2364
+ ${source.usage}->'cost'->>'output',
2365
+ ${source.usage}->'cost'->>'cacheRead',
2366
+ ${source.usage}->'cost'->>'cacheWrite'
2367
+ ) IS NOT NULL
2368
+ THEN COALESCE((${source.usage}->'cost'->>'input')::double precision, 0)
2369
+ + COALESCE((${source.usage}->'cost'->>'output')::double precision, 0)
2370
+ + COALESCE((${source.usage}->'cost'->>'cacheRead')::double precision, 0)
2371
+ + COALESCE((${source.usage}->'cost'->>'cacheWrite')::double precision, 0)
2372
+ ELSE NULL
2373
+ END
2374
+ `;
2765
2375
  }
2766
- function normalizeEmail(email) {
2767
- const normalized = email?.trim().toLowerCase();
2768
- return normalized || void 0;
2376
+ function conversationAggregateColumns(sources) {
2377
+ const metrics = sources?.metrics ?? juniorConversations;
2378
+ const roots = sources?.roots ?? juniorConversations;
2379
+ const conversationCount = sources ? sql5`COUNT(DISTINCT ${roots.conversationId})` : sql5`COUNT(*)`;
2380
+ return {
2381
+ active: sql5`${conversationCount} FILTER (
2382
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
2383
+ )::integer`,
2384
+ conversations: sql5`${conversationCount}::integer`,
2385
+ costUsd: sql5`SUM(${costValue(metrics)})::double precision`,
2386
+ durationMs: sql5`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
2387
+ failed: sql5`${conversationCount} FILTER (
2388
+ WHERE ${roots.executionStatus} = 'failed'
2389
+ )::integer`,
2390
+ tokens: sql5`SUM(${tokenValue(metrics)})::double precision`
2391
+ };
2769
2392
  }
2770
- function emptyTotals() {
2393
+ function conversationRangeColumns() {
2771
2394
  return {
2772
- active: 0,
2773
- activeDays: 0,
2774
- conversations: 0,
2775
- durationMs: 0,
2776
- failed: 0
2395
+ firstSeenAt: sql5`MIN(${juniorConversations.createdAt})`.mapWith(
2396
+ juniorConversations.createdAt
2397
+ ),
2398
+ lastSeenAt: sql5`MAX(${juniorConversations.lastActivityAt})`.mapWith(
2399
+ juniorConversations.lastActivityAt
2400
+ )
2777
2401
  };
2778
2402
  }
2779
- function emptyActivityDay(date) {
2403
+ function conversationActiveDaysColumn() {
2404
+ return sql5`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
2405
+ }
2406
+
2407
+ // src/api/conversations/stats.query.ts
2408
+ var WINDOW_DAYS = 90;
2409
+ var treeConversation = alias4(juniorConversations, "stats_tree_conversation");
2410
+ var treeAggregateColumns = conversationAggregateColumns({
2411
+ metrics: treeConversation,
2412
+ roots: juniorConversations
2413
+ });
2414
+ function emptyStatsItem(label) {
2780
2415
  return {
2781
2416
  active: 0,
2782
2417
  conversations: 0,
2783
- date,
2784
2418
  durationMs: 0,
2785
- failed: 0
2419
+ failed: 0,
2420
+ label
2786
2421
  };
2787
2422
  }
2788
- function activityDays2(days, nowMs) {
2789
- const items = [];
2790
- const end = new Date(nowMs);
2791
- end.setUTCHours(0, 0, 0, 0);
2792
- const start = new Date(end);
2793
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
2794
- for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2795
- const date = cursor.toISOString().slice(0, 10);
2796
- items.push(days.get(date) ?? emptyActivityDay(date));
2423
+ function addUsd(current, next) {
2424
+ return Math.round(((current ?? 0) + next) * 1e12) / 1e12;
2425
+ }
2426
+ function actorLabel(row) {
2427
+ return row.userEmail?.trim() || row.identityEmail?.trim() || row.userDisplayName?.trim() || row.identityDisplayName?.trim() || row.identityHandle?.trim() || row.identitySubjectId?.trim() || "Unknown";
2428
+ }
2429
+ function surfaceLabel(source) {
2430
+ if (source === "scheduler") return "Scheduler";
2431
+ if (source === "api") return "API";
2432
+ if (source === "internal" || source === "local") return "Internal";
2433
+ return "Conversation";
2434
+ }
2435
+ function locationLabel(row) {
2436
+ if (row.destinationProvider !== "slack") {
2437
+ return surfaceLabel(row.source);
2797
2438
  }
2798
- return items;
2439
+ if (row.destinationKind === "dm") {
2440
+ return "Direct Message";
2441
+ }
2442
+ if (row.destinationVisibility !== "public") {
2443
+ return "Private Conversation";
2444
+ }
2445
+ const name = (row.channelName ?? row.destinationDisplayName)?.trim().replace(/^#/, "");
2446
+ return name ? `#${name}` : "Public Channel";
2799
2447
  }
2800
- function statsItems2(map) {
2448
+ function addAggregate(map, label, row) {
2449
+ const item = map.get(label) ?? emptyStatsItem(label);
2450
+ item.active += row.active;
2451
+ item.conversations += row.conversations;
2452
+ item.durationMs += row.durationMs;
2453
+ item.failed += row.failed;
2454
+ if (row.tokens !== null) {
2455
+ item.tokens = (item.tokens ?? 0) + row.tokens;
2456
+ }
2457
+ if (row.costUsd !== null) {
2458
+ item.costUsd = addUsd(item.costUsd, row.costUsd);
2459
+ }
2460
+ map.set(label, item);
2461
+ }
2462
+ function statsItems(map) {
2801
2463
  return [...map.values()].sort(
2802
- (left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
2464
+ (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
2803
2465
  );
2804
2466
  }
2805
- function verifiedActorWhere(email) {
2806
- const normalizedEmail = normalizeEmail(email);
2807
- return and9(
2808
- eq10(juniorIdentities.provider, "slack"),
2809
- eq10(juniorIdentities.emailVerified, true),
2810
- sql8`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
2811
- normalizedEmail ? eq10(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0
2467
+ function statsWhere(start, end) {
2468
+ return and8(
2469
+ isNull4(juniorConversations.parentConversationId),
2470
+ gte(juniorConversations.lastActivityAt, start),
2471
+ lte(juniorConversations.lastActivityAt, end)
2812
2472
  );
2813
2473
  }
2814
- async function recentActorRows(email) {
2815
- return getDb().select({
2816
- channelName: juniorConversations.channelName,
2817
- conversationId: juniorConversations.conversationId,
2818
- createdAt: juniorConversations.createdAt,
2819
- destinationId: juniorDestinations.id,
2820
- durationMs: juniorConversations.durationMs,
2821
- email: juniorUsers.primaryEmailNormalized,
2822
- executionStatus: juniorConversations.executionStatus,
2823
- executionUpdatedAt: juniorConversations.executionUpdatedAt,
2824
- fullName: juniorUsers.displayName,
2825
- handle: juniorIdentities.handle,
2826
- lastActivityAt: juniorConversations.lastActivityAt,
2827
- providerSubjectId: juniorIdentities.providerSubjectId,
2828
- source: juniorConversations.source,
2829
- title: juniorConversations.title,
2830
- updatedAt: juniorConversations.updatedAt,
2831
- usage: juniorConversations.usage
2832
- }).from(juniorConversations).innerJoin(
2833
- juniorIdentities,
2834
- eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
2835
- ).innerJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).leftJoin(
2836
- juniorDestinations,
2837
- eq10(juniorDestinations.id, juniorConversations.destinationId)
2838
- ).where(
2839
- and9(verifiedActorWhere(email), isNull6(juniorConversations.archivedAt))
2840
- ).orderBy(
2841
- desc4(juniorConversations.lastActivityAt),
2842
- asc4(juniorConversations.conversationId)
2843
- ).limit(RECENT_LIMIT2);
2844
- }
2845
-
2846
- // src/api/people/list.query.ts
2847
- var DIRECTORY_ACTIVITY_DAYS = 90;
2848
- function activityWindow(nowMs) {
2474
+ function statsWindow(nowMs) {
2849
2475
  const end = new Date(nowMs);
2476
+ const start = new Date(nowMs);
2477
+ start.setUTCHours(0, 0, 0, 0);
2478
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2479
+ return { end, start };
2480
+ }
2481
+ function metricDays(rows, endMs) {
2482
+ const byDate = new Map(rows.map((row) => [row.date, row]));
2483
+ const end = new Date(endMs);
2850
2484
  end.setUTCHours(0, 0, 0, 0);
2851
2485
  const start = new Date(end);
2852
- start.setUTCDate(start.getUTCDate() - (DIRECTORY_ACTIVITY_DAYS - 1));
2853
- return { end, start };
2486
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2487
+ const days = [];
2488
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2489
+ const date = cursor.toISOString().slice(0, 10);
2490
+ const row = byDate.get(date);
2491
+ days.push({
2492
+ conversations: row?.conversations ?? 0,
2493
+ date,
2494
+ durationMs: row?.durationMs ?? 0,
2495
+ ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: addUsd(void 0, row.costUsd) } : {},
2496
+ ...row?.tokens !== null && row?.tokens !== void 0 ? { tokens: row.tokens } : {}
2497
+ });
2498
+ }
2499
+ return days;
2854
2500
  }
2855
- function directoryActivityDays2(rows, nowMs) {
2856
- const days = new Map(rows.map((row) => [row.date, row]));
2857
- const { end, start } = activityWindow(nowMs);
2858
- const items = [];
2501
+ function guardianStats(rows, endMs) {
2502
+ const byDate = new Map(rows.map((row) => [row.date, row]));
2503
+ const end = new Date(endMs);
2504
+ end.setUTCHours(0, 0, 0, 0);
2505
+ const start = new Date(end);
2506
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2507
+ const metricDays2 = [];
2508
+ let allow = 0;
2509
+ let ask = 0;
2510
+ let deny = 0;
2511
+ let requests = 0;
2512
+ let costUsd;
2859
2513
  for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2860
2514
  const date = cursor.toISOString().slice(0, 10);
2861
- items.push(days.get(date) ?? { activePeople: 0, conversations: 0, date });
2515
+ const row = byDate.get(date);
2516
+ allow += row?.allow ?? 0;
2517
+ ask += row?.ask ?? 0;
2518
+ deny += row?.deny ?? 0;
2519
+ requests += row?.requests ?? 0;
2520
+ if (row?.costUsd !== null && row?.costUsd !== void 0) {
2521
+ costUsd = addUsd(costUsd, row.costUsd);
2522
+ }
2523
+ metricDays2.push({
2524
+ allow: row?.allow ?? 0,
2525
+ ask: row?.ask ?? 0,
2526
+ date,
2527
+ deny: row?.deny ?? 0,
2528
+ requests: row?.requests ?? 0,
2529
+ ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: row.costUsd } : {}
2530
+ });
2862
2531
  }
2863
- return items;
2532
+ return {
2533
+ allow,
2534
+ ask,
2535
+ deny,
2536
+ metricDays: metricDays2,
2537
+ requests,
2538
+ ...costUsd !== void 0 ? { costUsd } : {}
2539
+ };
2864
2540
  }
2865
- async function readPeopleListFromSql() {
2866
- const nowMs = Date.now();
2867
- const { end, start } = activityWindow(nowMs);
2868
- const activityDate = sql9`TO_CHAR(
2541
+ async function aggregateStats(db, start, end) {
2542
+ const where = statsWhere(start, end);
2543
+ const activityDate = sql6`TO_CHAR(
2869
2544
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2870
2545
  'YYYY-MM-DD'
2871
2546
  )`;
2872
- const [rows, activityRows] = await Promise.all([
2873
- getDb().select({
2874
- email: juniorUsers.primaryEmailNormalized,
2875
- fullName: juniorUsers.displayName,
2876
- slackUserId: sql9`MAX(${juniorIdentities.providerSubjectId})`,
2877
- slackUserName: sql9`MAX(${juniorIdentities.handle})`,
2878
- activeDays: conversationActiveDaysColumn(),
2879
- ...peopleTreeAggregateColumns,
2880
- ...conversationRangeColumns()
2547
+ const [totalsRows, actorRows, locationRows, metricRows] = await Promise.all([
2548
+ db.select(treeAggregateColumns).from(juniorConversations).innerJoin(
2549
+ treeConversation,
2550
+ eq9(
2551
+ treeConversation.rootConversationId,
2552
+ juniorConversations.conversationId
2553
+ )
2554
+ ).where(where),
2555
+ db.select({
2556
+ identityDisplayName: juniorIdentities.displayName,
2557
+ identityEmail: juniorIdentities.emailNormalized,
2558
+ identityHandle: juniorIdentities.handle,
2559
+ identitySubjectId: juniorIdentities.providerSubjectId,
2560
+ userDisplayName: juniorUsers.displayName,
2561
+ userEmail: juniorUsers.primaryEmailNormalized,
2562
+ ...treeAggregateColumns
2881
2563
  }).from(juniorConversations).innerJoin(
2564
+ treeConversation,
2565
+ eq9(
2566
+ treeConversation.rootConversationId,
2567
+ juniorConversations.conversationId
2568
+ )
2569
+ ).leftJoin(
2882
2570
  juniorIdentities,
2883
- eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2884
- ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, peopleTreeMetricsJoin()).where(verifiedActorWhere()).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
2885
- getDb().select({
2886
- activePeople: sql9`COUNT(DISTINCT ${juniorUsers.id})::int`,
2887
- conversations: sql9`COUNT(*)::int`,
2888
- date: activityDate
2571
+ eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
2572
+ ).leftJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
2573
+ juniorIdentities.displayName,
2574
+ juniorIdentities.emailNormalized,
2575
+ juniorIdentities.handle,
2576
+ juniorIdentities.providerSubjectId,
2577
+ juniorUsers.displayName,
2578
+ juniorUsers.primaryEmailNormalized
2579
+ ),
2580
+ db.select({
2581
+ channelName: juniorConversations.channelName,
2582
+ destinationDisplayName: juniorDestinations.displayName,
2583
+ destinationKind: juniorDestinations.kind,
2584
+ destinationProvider: juniorDestinations.provider,
2585
+ destinationVisibility: juniorDestinations.visibility,
2586
+ source: juniorConversations.source,
2587
+ ...treeAggregateColumns
2889
2588
  }).from(juniorConversations).innerJoin(
2890
- juniorIdentities,
2891
- eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2892
- ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).where(
2893
- and10(
2894
- verifiedActorWhere(),
2895
- gte3(juniorConversations.lastActivityAt, start)
2589
+ treeConversation,
2590
+ eq9(
2591
+ treeConversation.rootConversationId,
2592
+ juniorConversations.conversationId
2896
2593
  )
2897
- ).groupBy(activityDate)
2594
+ ).leftJoin(
2595
+ juniorDestinations,
2596
+ eq9(juniorDestinations.id, juniorConversations.destinationId)
2597
+ ).where(where).groupBy(
2598
+ juniorConversations.channelName,
2599
+ juniorConversations.source,
2600
+ juniorDestinations.displayName,
2601
+ juniorDestinations.kind,
2602
+ juniorDestinations.provider,
2603
+ juniorDestinations.visibility
2604
+ ),
2605
+ db.select({
2606
+ conversations: treeAggregateColumns.conversations,
2607
+ costUsd: treeAggregateColumns.costUsd,
2608
+ date: activityDate,
2609
+ durationMs: treeAggregateColumns.durationMs,
2610
+ tokens: treeAggregateColumns.tokens
2611
+ }).from(juniorConversations).innerJoin(
2612
+ treeConversation,
2613
+ eq9(
2614
+ treeConversation.rootConversationId,
2615
+ juniorConversations.conversationId
2616
+ )
2617
+ ).where(where).groupBy(activityDate)
2898
2618
  ]);
2899
- const people = rows.map((row) => {
2900
- const actor = {
2901
- email: row.email,
2902
- ...row.fullName ? { fullName: row.fullName } : {},
2903
- ...row.slackUserId ? { slackUserId: row.slackUserId } : {},
2904
- ...row.slackUserName ? { slackUserName: row.slackUserName } : {}
2905
- };
2906
- return {
2907
- active: row.active,
2908
- activeDays: row.activeDays,
2909
- conversations: row.conversations,
2910
- durationMs: row.durationMs,
2911
- failed: row.failed,
2912
- firstSeenAt: row.firstSeenAt.toISOString(),
2913
- lastSeenAt: row.lastSeenAt.toISOString(),
2914
- actor,
2915
- ...row.tokens !== null ? { tokens: row.tokens } : {}
2916
- };
2917
- });
2619
+ return { actorRows, locationRows, metricRows, totals: totalsRows[0] };
2620
+ }
2621
+ async function aggregateGuardianStats(db, start, end) {
2622
+ const date = sql6`TO_CHAR(
2623
+ ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
2624
+ 'YYYY-MM-DD'
2625
+ )`;
2626
+ const decision = sql6`${juniorConversationEvents.payload}->>'decision'`;
2627
+ const cost2 = sql6`CASE
2628
+ WHEN jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
2629
+ THEN (${juniorConversationEvents.payload}->>'costUsd')::double precision
2630
+ ELSE NULL
2631
+ END`;
2632
+ return await db.select({
2633
+ allow: sql6`COUNT(*) FILTER (WHERE ${decision} = 'allow')::integer`,
2634
+ ask: sql6`COUNT(*) FILTER (WHERE ${decision} = 'ask')::integer`,
2635
+ costUsd: sql6`SUM(${cost2})::double precision`,
2636
+ date,
2637
+ deny: sql6`COUNT(*) FILTER (WHERE ${decision} = 'deny')::integer`,
2638
+ requests: sql6`COUNT(*)::integer`
2639
+ }).from(juniorConversationEvents).where(
2640
+ and8(
2641
+ eq9(juniorConversationEvents.type, "guardian_action_reviewed"),
2642
+ gte(juniorConversationEvents.createdAt, start),
2643
+ lte(juniorConversationEvents.createdAt, end)
2644
+ )
2645
+ ).groupBy(date);
2646
+ }
2647
+ async function readConversationStatsFromSql() {
2648
+ const nowMs = Date.now();
2649
+ const { end, start } = statsWindow(nowMs);
2650
+ const db = getDb();
2651
+ const [{ actorRows, locationRows, metricRows, totals }, guardianRows] = await Promise.all([
2652
+ aggregateStats(db, start, end),
2653
+ aggregateGuardianStats(db, start, end)
2654
+ ]);
2655
+ const actors = /* @__PURE__ */ new Map();
2656
+ const locations = /* @__PURE__ */ new Map();
2657
+ for (const row of actorRows) {
2658
+ addAggregate(actors, actorLabel(row), row);
2659
+ }
2660
+ for (const row of locationRows) {
2661
+ addAggregate(locations, locationLabel(row), row);
2662
+ }
2918
2663
  return {
2919
- activityDays: directoryActivityDays2(activityRows, nowMs),
2664
+ active: totals?.active ?? 0,
2665
+ conversations: totals?.conversations ?? 0,
2666
+ durationMs: totals?.durationMs ?? 0,
2667
+ failed: totals?.failed ?? 0,
2920
2668
  generatedAt: new Date(nowMs).toISOString(),
2921
- people: people.sort(
2922
- (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.actor.email.localeCompare(right.actor.email)
2923
- ),
2669
+ guardian: guardianStats(guardianRows, nowMs),
2670
+ metricDays: metricDays(metricRows, nowMs),
2671
+ locations: statsItems(locations),
2672
+ actors: statsItems(actors),
2924
2673
  source: "conversation_index",
2674
+ ...totals?.costUsd !== null && totals?.costUsd !== void 0 ? { costUsd: addUsd(void 0, totals.costUsd) } : {},
2675
+ ...totals?.tokens !== null && totals?.tokens !== void 0 ? { tokens: totals.tokens } : {},
2925
2676
  windowEnd: end.toISOString(),
2926
2677
  windowStart: start.toISOString()
2927
2678
  };
2928
2679
  }
2929
2680
 
2930
- // src/api/people/list.ts
2931
- async function readPeopleList() {
2932
- return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
2933
- }
2934
- var list_default3 = defineApiRoute({
2935
- method: "get",
2936
- path: "/",
2937
- responseSchema: actorDirectoryReportSchema,
2938
- handler: readPeopleList
2681
+ // src/api/conversations/stats.ts
2682
+ async function readConversationStats() {
2683
+ return conversationStatsReportSchema.parse(
2684
+ await readConversationStatsFromSql()
2685
+ );
2686
+ }
2687
+ var stats_default = defineApiRoute({
2688
+ method: "get",
2689
+ path: "/stats",
2690
+ responseSchema: conversationStatsReportSchema,
2691
+ handler: readConversationStats
2939
2692
  });
2940
2693
 
2941
- // src/api/people/profile.query.ts
2942
- import { and as and11, eq as eq12, gte as gte4, sql as sql10 } from "drizzle-orm";
2943
- function emptyProfile(email, nowMs) {
2944
- const end = new Date(nowMs);
2945
- end.setUTCHours(0, 0, 0, 0);
2946
- const start = new Date(end);
2947
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
2948
- return {
2949
- activityDays: activityDays2(/* @__PURE__ */ new Map(), nowMs),
2694
+ // src/api/conversations/routes.ts
2695
+ var routes = [
2696
+ list_default,
2697
+ stats_default,
2698
+ archive_default,
2699
+ event_list_default,
2700
+ detail_default
2701
+ ];
2702
+ function createConversationRoutes() {
2703
+ const app = new Hono();
2704
+ registerApiRoutes(app, routes);
2705
+ return app;
2706
+ }
2707
+
2708
+ // src/reporting.ts
2709
+ import { readFileSync } from "fs";
2710
+ import path from "path";
2711
+ function readDescriptionText(home) {
2712
+ try {
2713
+ const raw = readFileSync(path.join(home, "DESCRIPTION.md"), "utf8").trim();
2714
+ return raw || void 0;
2715
+ } catch {
2716
+ return void 0;
2717
+ }
2718
+ }
2719
+ async function readSkillReports() {
2720
+ const { discoverSkills } = await import("./skills-2S3MDFIQ.js");
2721
+ const skills = await discoverSkills();
2722
+ return skillReportsSchema.parse(
2723
+ skills.map((skill) => ({
2724
+ name: skill.name,
2725
+ pluginProvider: skill.pluginProvider
2726
+ }))
2727
+ );
2728
+ }
2729
+ async function readPlugins() {
2730
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-QLAC4Q3W.js");
2731
+ return pluginsSchema.parse(
2732
+ pluginCatalogRuntime.getProviders().map((plugin) => ({
2733
+ configKeys: plugin.manifest.configKeys ?? [],
2734
+ description: plugin.manifest.description,
2735
+ displayName: plugin.manifest.displayName,
2736
+ name: plugin.manifest.name
2737
+ }))
2738
+ );
2739
+ }
2740
+ async function readRuntimeInfoReport() {
2741
+ const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
2742
+ import("./discovery-GUFTFBTL.js"),
2743
+ import("./catalog-runtime-QLAC4Q3W.js"),
2744
+ readPlugins(),
2745
+ readSkillReports()
2746
+ ]);
2747
+ const home = homeDir();
2748
+ return runtimeInfoReportSchema.parse({
2749
+ cwd: process.cwd(),
2750
+ homeDir: home,
2751
+ descriptionText: readDescriptionText(home),
2752
+ providers: plugins.map((plugin) => plugin.name),
2753
+ skills,
2754
+ packagedContent: pluginCatalogRuntime.getPackageContent()
2755
+ });
2756
+ }
2757
+ async function readPluginOperationalReportFeed() {
2758
+ const nowMs = Date.now();
2759
+ const { getPluginOperationalReports } = await import("./agent-hooks-L7D2BDVV.js");
2760
+ return pluginOperationalReportFeedSchema.parse({
2761
+ source: "plugins",
2950
2762
  generatedAt: new Date(nowMs).toISOString(),
2951
- locations: [],
2952
- recentConversations: [],
2953
- actor: { email },
2954
- source: "conversation_index",
2955
- surfaces: [],
2956
- totals: emptyTotals(),
2957
- windowEnd: end.toISOString(),
2958
- windowStart: start.toISOString()
2763
+ reports: await getPluginOperationalReports(nowMs)
2764
+ });
2765
+ }
2766
+
2767
+ // src/api/health.ts
2768
+ var health_default = defineApiRoute({
2769
+ method: "get",
2770
+ path: "/api/health",
2771
+ responseSchema: healthReportSchema,
2772
+ handler: readHealthReport
2773
+ });
2774
+
2775
+ // src/api/locations/routes.ts
2776
+ import { Hono as Hono2 } from "hono";
2777
+
2778
+ // src/api/locations/query.ts
2779
+ import { and as and9, asc as asc3, desc as desc3, eq as eq10, gte as gte2, isNull as isNull5, sql as sql7 } from "drizzle-orm";
2780
+ import { alias as alias5 } from "drizzle-orm/pg-core";
2781
+
2782
+ // src/api/conversations/reporting.ts
2783
+ function surfaceLabel2(surface) {
2784
+ if (surface === "scheduler") return "Scheduler";
2785
+ if (surface === "api") return "API";
2786
+ if (surface === "internal") return "Internal";
2787
+ return "Conversation";
2788
+ }
2789
+ function slackLocationLabel(args) {
2790
+ const channelId = args.channel;
2791
+ if (!channelId) return void 0;
2792
+ if (args.channelNameRedacted && args.channelName) return args.channelName;
2793
+ const name = args.channelName?.replace(/^#/, "");
2794
+ if (channelId.startsWith("D")) return "Direct Message";
2795
+ if (channelId.startsWith("C")) {
2796
+ return name ? `#${name}` : "Public Channel";
2797
+ }
2798
+ if (channelId.startsWith("G")) {
2799
+ if (name?.startsWith("mpdm-")) return "Group DM";
2800
+ return "Private Channel";
2801
+ }
2802
+ return name || channelId;
2803
+ }
2804
+ function actorFromRow(row) {
2805
+ const actor = {
2806
+ ...row.email ? { email: row.email } : {},
2807
+ ...row.fullName ? { fullName: row.fullName } : {},
2808
+ ...row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
2809
+ ...row.handle ? { slackUserName: row.handle } : {}
2959
2810
  };
2811
+ return Object.keys(actor).length ? actor : void 0;
2960
2812
  }
2961
- function addAggregate2(map, label, row) {
2962
- const item = map.get(label) ?? {
2813
+ function summaryFromRow(row, options = {}) {
2814
+ const actor = actorFromRow(row);
2815
+ const conversation = {
2816
+ conversationId: row.conversationId,
2817
+ createdAtMs: row.createdAt.getTime(),
2818
+ lastActivityAtMs: row.lastActivityAt.getTime(),
2819
+ updatedAtMs: row.updatedAt.getTime(),
2820
+ execution: {
2821
+ status: row.executionStatus,
2822
+ ...row.executionUpdatedAt ? { updatedAtMs: row.executionUpdatedAt.getTime() } : {}
2823
+ },
2824
+ ...actor ? { actor } : {},
2825
+ ...row.channelName ? { channelName: row.channelName } : {},
2826
+ ...row.source ? { source: row.source } : {},
2827
+ ...row.title ? { title: row.title } : {}
2828
+ };
2829
+ return conversationSummaryFromStoredConversation({
2830
+ access: options.access,
2831
+ conversation,
2832
+ durationMs: options.metrics?.durationMs ?? row.durationMs,
2833
+ ...options.access?.visibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
2834
+ usage: options.metrics?.usage ?? row.usage ?? void 0
2835
+ });
2836
+ }
2837
+
2838
+ // src/api/locations/query.ts
2839
+ var RECENT_LIMIT = 25;
2840
+ var ACTIVITY_DAYS = 90;
2841
+ var treeConversation2 = alias5(
2842
+ juniorConversations,
2843
+ "location_tree_conversation"
2844
+ );
2845
+ var treeAggregateColumns2 = conversationAggregateColumns({
2846
+ metrics: treeConversation2,
2847
+ roots: juniorConversations
2848
+ });
2849
+ function emptyMetrics() {
2850
+ return {
2963
2851
  active: 0,
2964
2852
  conversations: 0,
2965
2853
  durationMs: 0,
2966
- failed: 0,
2967
- label
2854
+ failed: 0
2968
2855
  };
2969
- item.active += row.active;
2970
- item.conversations += row.conversations;
2971
- item.durationMs += row.durationMs;
2972
- item.failed += row.failed;
2973
- if (row.tokens !== null) item.tokens = (item.tokens ?? 0) + row.tokens;
2974
- map.set(label, item);
2975
2856
  }
2976
- function surfaceExpression() {
2977
- return sql10`CASE
2978
- WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
2979
- THEN ${juniorConversations.source}
2980
- WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
2981
- WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
2982
- WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
2983
- ELSE 'internal'
2984
- END`;
2857
+ function addMetrics(target, row) {
2858
+ target.active += row.active;
2859
+ target.conversations += row.conversations;
2860
+ target.durationMs += row.durationMs;
2861
+ target.failed += row.failed;
2862
+ if (row.tokens !== null) {
2863
+ target.tokens = (target.tokens ?? 0) + row.tokens;
2864
+ }
2865
+ if (row.costUsd !== null) {
2866
+ target.costUsd = Math.round(((target.costUsd ?? 0) + row.costUsd) * 1e12) / 1e12;
2867
+ }
2985
2868
  }
2986
- function locationLabel2(row) {
2987
- if (row.surface !== "slack")
2988
- return surfaceLabel2(row.surface);
2989
- if (row.destinationVisibility !== "public") return "Private Conversation";
2990
- return slackLocationLabel({
2991
- channel: row.channel || void 0,
2992
- channelName: row.channelName ?? void 0
2993
- }) ?? "Conversation";
2869
+ function publicLabel(input) {
2870
+ const name = input.displayName?.trim().replace(/^#/, "");
2871
+ return name ? `#${name}` : `Public channel ${input.providerDestinationId}`;
2994
2872
  }
2995
- async function readPeopleProfileFromSql(email, options = {}) {
2996
- const nowMs = Date.now();
2997
- const normalizedEmail = normalizeEmail(email);
2998
- if (!normalizedEmail) return emptyProfile("", nowMs);
2999
- const end = new Date(nowMs);
3000
- end.setUTCHours(0, 0, 0, 0);
3001
- const start = new Date(end);
3002
- start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3003
- const where = verifiedActorWhere(normalizedEmail);
3004
- const metricsJoin = peopleTreeMetricsJoin();
3005
- const surface = surfaceExpression();
3006
- const activityDate = sql10`TO_CHAR(
3007
- ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3008
- 'YYYY-MM-DD'
3009
- )`;
3010
- const channel = sql10`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
3011
- const [totalsRows, dayRows, locationRows, surfaceRows, recentRows] = await Promise.all([
3012
- getDb().select({
3013
- email: juniorUsers.primaryEmailNormalized,
3014
- fullName: juniorUsers.displayName,
3015
- slackUserId: sql10`MAX(${juniorIdentities.providerSubjectId})`,
3016
- slackUserName: sql10`MAX(${juniorIdentities.handle})`,
3017
- activeDays: conversationActiveDaysColumn(),
3018
- ...peopleTreeAggregateColumns
3019
- }).from(juniorConversations).innerJoin(
3020
- juniorIdentities,
3021
- eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3022
- ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
3023
- getDb().select({
3024
- date: activityDate,
3025
- ...peopleTreeAggregateColumns
3026
- }).from(juniorConversations).innerJoin(
3027
- juniorIdentities,
3028
- eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3029
- ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(and11(where, gte4(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
3030
- getDb().select({
3031
- channel,
3032
- channelName: juniorConversations.channelName,
3033
- destinationVisibility: juniorDestinations.visibility,
3034
- surface,
3035
- ...peopleTreeAggregateColumns
3036
- }).from(juniorConversations).innerJoin(
3037
- juniorIdentities,
3038
- eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3039
- ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).leftJoin(
3040
- juniorDestinations,
3041
- eq12(juniorDestinations.id, juniorConversations.destinationId)
3042
- ).where(where).groupBy(
3043
- channel,
3044
- juniorConversations.channelName,
3045
- juniorDestinations.visibility,
3046
- surface
3047
- ),
3048
- getDb().select({ surface, ...peopleTreeAggregateColumns }).from(juniorConversations).innerJoin(
3049
- juniorIdentities,
3050
- eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3051
- ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(surface),
3052
- recentActorRows(normalizedEmail)
3053
- ]);
3054
- const totalsRow = totalsRows[0];
3055
- if (!totalsRow) return emptyProfile(normalizedEmail, nowMs);
3056
- const actor = {
3057
- email: totalsRow.email,
3058
- ...totalsRow.fullName ? { fullName: totalsRow.fullName } : {},
3059
- ...totalsRow.slackUserId ? { slackUserId: totalsRow.slackUserId } : {},
3060
- ...totalsRow.slackUserName ? { slackUserName: totalsRow.slackUserName } : {}
3061
- };
3062
- const days = /* @__PURE__ */ new Map();
3063
- for (const row of dayRows) {
3064
- days.set(row.date, {
3065
- active: row.active,
3066
- conversations: row.conversations,
3067
- ...row.costUsd !== null ? { costUsd: row.costUsd } : {},
3068
- date: row.date,
3069
- durationMs: row.durationMs,
3070
- failed: row.failed,
3071
- ...row.tokens !== null ? { tokens: row.tokens } : {}
3072
- });
3073
- }
3074
- const locations = /* @__PURE__ */ new Map();
3075
- for (const row of locationRows) {
3076
- addAggregate2(locations, locationLabel2(row), row);
3077
- }
3078
- const surfaces = /* @__PURE__ */ new Map();
3079
- for (const row of surfaceRows) {
3080
- addAggregate2(
3081
- surfaces,
3082
- surfaceLabel2(row.surface),
3083
- row
3084
- );
3085
- }
3086
- const recentConversationIds = recentRows.map((row) => row.conversationId);
3087
- const [accessByConversation, metricsByRoot] = await Promise.all([
3088
- readConversationAccessFromSql(
3089
- getDb(),
3090
- recentConversationIds,
3091
- options.verifiedViewerEmail
3092
- ),
3093
- readRootConversationMetricsFromSql(getDb(), recentConversationIds)
3094
- ]);
2873
+ function actorLabel2(actor) {
2874
+ return actor.email?.trim() || actor.fullName?.trim() || actor.slackUserName?.trim() || actor.slackUserId?.trim() || "Unknown";
2875
+ }
2876
+ function emptyActor(actor) {
3095
2877
  return {
3096
- activityDays: activityDays2(days, nowMs),
3097
- generatedAt: new Date(nowMs).toISOString(),
3098
- locations: statsItems2(locations),
3099
- recentConversations: recentRows.map(
3100
- (row) => summaryFromRow(row, {
3101
- access: accessByConversation.get(row.conversationId),
3102
- metrics: metricsByRoot.get(row.conversationId)
3103
- })
3104
- ),
2878
+ ...emptyMetrics(),
3105
2879
  actor,
3106
- source: "conversation_index",
3107
- surfaces: statsItems2(surfaces),
3108
- totals: {
3109
- active: totalsRow.active,
3110
- activeDays: totalsRow.activeDays,
3111
- conversations: totalsRow.conversations,
3112
- durationMs: totalsRow.durationMs,
3113
- failed: totalsRow.failed,
3114
- ...totalsRow.tokens !== null ? { tokens: totalsRow.tokens } : {}
3115
- },
3116
- windowEnd: end.toISOString(),
3117
- windowStart: start.toISOString()
2880
+ label: actorLabel2(actor)
3118
2881
  };
3119
2882
  }
3120
-
3121
- // src/api/people/profile.ts
3122
- async function readPeopleProfile(email, options = {}) {
3123
- return actorProfileReportSchema.parse(
3124
- await readPeopleProfileFromSql(email, options)
2883
+ function topLevelWhere() {
2884
+ return isNull5(juniorConversations.parentConversationId);
2885
+ }
2886
+ function publicLocationWhere(destinationId) {
2887
+ return and9(
2888
+ topLevelWhere(),
2889
+ eq10(juniorDestinations.id, destinationId),
2890
+ eq10(juniorDestinations.visibility, "public")
3125
2891
  );
3126
2892
  }
3127
- var profile_default = defineApiRoute({
3128
- method: "get",
3129
- path: "/:email",
3130
- responseSchema: actorProfileReportSchema,
3131
- handler: async (c) => {
3132
- const { email } = parseParams(personParamsSchema, c.req.param());
3133
- const verifiedViewerEmail = c.get("verifiedViewerEmail");
3134
- return readPeopleProfile(
3135
- email,
3136
- verifiedViewerEmail ? { verifiedViewerEmail } : {}
3137
- );
2893
+ function locationColumns() {
2894
+ return {
2895
+ destinationDisplayName: juniorDestinations.displayName,
2896
+ destinationId: juniorDestinations.id,
2897
+ destinationKind: juniorDestinations.kind,
2898
+ destinationProvider: juniorDestinations.provider,
2899
+ destinationProviderId: juniorDestinations.providerDestinationId,
2900
+ destinationVisibility: juniorDestinations.visibility
2901
+ };
2902
+ }
2903
+ function locationGroupBy() {
2904
+ return [
2905
+ juniorDestinations.displayName,
2906
+ juniorDestinations.id,
2907
+ juniorDestinations.kind,
2908
+ juniorDestinations.provider,
2909
+ juniorDestinations.providerDestinationId,
2910
+ juniorDestinations.visibility
2911
+ ];
2912
+ }
2913
+ function locationFromAggregate(row) {
2914
+ if (row.destinationVisibility !== "public" || !row.destinationId || !row.destinationProvider || !row.destinationProviderId || !row.destinationKind) {
2915
+ return void 0;
3138
2916
  }
3139
- });
3140
-
3141
- // src/api/people/spend.query.ts
3142
- import { and as and12, eq as eq13, gte as gte5, lte as lte2, sql as sql11 } from "drizzle-orm";
3143
- var SEVEN_DAY_OFFSET = 6;
3144
- var THIRTY_DAY_OFFSET = 29;
3145
- function addUsd2(current, next) {
3146
- return Math.round((current + next) * 1e12) / 1e12;
2917
+ const location = {
2918
+ ...emptyMetrics(),
2919
+ firstSeenAt: row.firstSeenAt.toISOString(),
2920
+ id: row.destinationId,
2921
+ kind: row.destinationKind,
2922
+ label: publicLabel({
2923
+ displayName: row.destinationDisplayName,
2924
+ providerDestinationId: row.destinationProviderId
2925
+ }),
2926
+ lastSeenAt: row.lastSeenAt.toISOString(),
2927
+ provider: row.destinationProvider,
2928
+ providerDestinationId: row.destinationProviderId,
2929
+ visibility: "public"
2930
+ };
2931
+ addMetrics(location, row);
2932
+ return location;
3147
2933
  }
3148
- function spendWindow(nowMs) {
3149
- const end = new Date(nowMs);
3150
- const currentDay = new Date(end);
3151
- currentDay.setUTCHours(0, 0, 0, 0);
3152
- const sevenDayStart = new Date(currentDay);
3153
- sevenDayStart.setUTCDate(sevenDayStart.getUTCDate() - SEVEN_DAY_OFFSET);
3154
- const thirtyDayStart = new Date(currentDay);
3155
- thirtyDayStart.setUTCDate(thirtyDayStart.getUTCDate() - THIRTY_DAY_OFFSET);
3156
- return { end, sevenDayStart, thirtyDayStart };
2934
+ async function directoryRows(db) {
2935
+ return db.select({
2936
+ ...locationColumns(),
2937
+ ...treeAggregateColumns2,
2938
+ ...conversationRangeColumns()
2939
+ }).from(juniorConversations).innerJoin(
2940
+ treeConversation2,
2941
+ eq10(
2942
+ treeConversation2.rootConversationId,
2943
+ juniorConversations.conversationId
2944
+ )
2945
+ ).leftJoin(
2946
+ juniorDestinations,
2947
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
2948
+ ).where(topLevelWhere()).groupBy(...locationGroupBy());
3157
2949
  }
3158
- async function readPersonalSpendFromSql(email, nowMs = Date.now()) {
3159
- const normalizedEmail = normalizeEmail(email);
3160
- const { end, sevenDayStart, thirtyDayStart } = spendWindow(nowMs);
3161
- const activityDate = sql11`TO_CHAR(
2950
+ async function directoryActivityRows(db, start) {
2951
+ const date = sql7`TO_CHAR(
3162
2952
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3163
2953
  'YYYY-MM-DD'
3164
2954
  )`;
3165
- const metricsJoin = peopleTreeMetricsJoin();
3166
- const rows = normalizedEmail ? await getDb().select({
3167
- costUsd: peopleTreeAggregateColumns.costUsd,
3168
- date: activityDate
3169
- }).from(juniorConversations).innerJoin(
3170
- juniorIdentities,
3171
- eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3172
- ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(
3173
- and12(
3174
- verifiedActorWhere(normalizedEmail),
3175
- gte5(juniorConversations.lastActivityAt, thirtyDayStart),
3176
- lte2(juniorConversations.lastActivityAt, end)
3177
- )
3178
- ).groupBy(activityDate) : [];
3179
- const sevenDayStartDate = sevenDayStart.toISOString().slice(0, 10);
3180
- let sevenDaysUsd = 0;
3181
- let thirtyDaysUsd = 0;
3182
- for (const row of rows) {
3183
- if (row.costUsd === null) continue;
3184
- thirtyDaysUsd = addUsd2(thirtyDaysUsd, row.costUsd);
3185
- if (row.date >= sevenDayStartDate) {
3186
- sevenDaysUsd = addUsd2(sevenDaysUsd, row.costUsd);
3187
- }
3188
- }
3189
- return {
3190
- generatedAt: new Date(nowMs).toISOString(),
3191
- sevenDaysUsd,
3192
- source: "conversation_index",
3193
- thirtyDaysUsd,
3194
- windowEnd: end.toISOString(),
3195
- windowStart: thirtyDayStart.toISOString()
3196
- };
2955
+ return db.select({
2956
+ conversations: sql7`COUNT(*)::integer`,
2957
+ date,
2958
+ visibility: juniorDestinations.visibility
2959
+ }).from(juniorConversations).leftJoin(
2960
+ juniorDestinations,
2961
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
2962
+ ).where(and9(topLevelWhere(), gte2(juniorConversations.lastActivityAt, start))).groupBy(date, juniorDestinations.visibility);
3197
2963
  }
3198
-
3199
- // src/api/people/spend.ts
3200
- var PERSONAL_SPEND_CACHE_TTL_MS = 5 * 6e4;
3201
- function createPersonalSpendRoute() {
3202
- const cache = /* @__PURE__ */ new Map();
3203
- async function read(email) {
3204
- const normalizedEmail = email.trim().toLowerCase();
3205
- const nowMs = Date.now();
3206
- const cached = cache.get(normalizedEmail);
3207
- if (cached && cached.expiresAtMs > nowMs) {
3208
- return cached.report;
3209
- }
3210
- cache.delete(normalizedEmail);
3211
- const report = readPersonalSpendFromSql(normalizedEmail, nowMs);
3212
- const entry = {
3213
- expiresAtMs: nowMs + PERSONAL_SPEND_CACHE_TTL_MS,
3214
- report
2964
+ function directoryActivityDays(rows, nowMs) {
2965
+ const days = /* @__PURE__ */ new Map();
2966
+ for (const row of rows) {
2967
+ const day = days.get(row.date) ?? {
2968
+ date: row.date,
2969
+ privateConversations: 0,
2970
+ publicConversations: 0
3215
2971
  };
3216
- cache.set(normalizedEmail, entry);
3217
- try {
3218
- return await report;
3219
- } catch (error) {
3220
- if (cache.get(normalizedEmail) === entry) cache.delete(normalizedEmail);
3221
- throw error;
2972
+ if (row.visibility === "public") {
2973
+ day.publicConversations += row.conversations;
2974
+ } else {
2975
+ day.privateConversations += row.conversations;
3222
2976
  }
2977
+ days.set(row.date, day);
3223
2978
  }
3224
- return defineApiRoute({
3225
- method: "get",
3226
- path: "/me/spend",
3227
- responseSchema: personalSpendReportSchema,
3228
- handler: async (context) => {
3229
- const email = context.get("verifiedViewerEmail");
3230
- if (!email) throwApiError(403, "Verified viewer email required.");
3231
- return read(email);
3232
- }
3233
- });
3234
- }
3235
-
3236
- // src/api/people/routes.ts
3237
- function createPeopleRoutes() {
3238
- const app = new Hono3();
3239
- const routes5 = [
3240
- createPersonalSpendRoute(),
3241
- list_default3,
3242
- profile_default
3243
- ];
3244
- registerApiRoutes(app, routes5);
3245
- return app;
3246
- }
3247
-
3248
- // src/api/plugin-reports.ts
3249
- var plugin_reports_default = defineApiRoute({
3250
- method: "get",
3251
- path: "/api/plugin-reports",
3252
- responseSchema: pluginOperationalReportFeedSchema,
3253
- handler: readPluginOperationalReportFeed
3254
- });
3255
-
3256
- // src/api/personal-tokens/routes.ts
3257
- import { Hono as Hono4 } from "hono";
3258
-
3259
- // src/personal-tokens/store.ts
3260
- import { createHash, randomBytes, randomUUID } from "crypto";
3261
- import { and as and13, desc as desc5, eq as eq14, gt, isNull as isNull7 } from "drizzle-orm";
3262
- var TOKEN_PREFIX = "jr_pat_";
3263
- var TOKEN_SUFFIX_LENGTH = 8;
3264
- var DEFAULT_TOKEN_LIFETIME_MS = 90 * 24 * 60 * 60 * 1e3;
3265
- function normalizeEmail2(email) {
3266
- return email.trim().toLowerCase();
3267
- }
3268
- function hashToken(token2) {
3269
- return createHash("sha256").update(token2, "utf8").digest("hex");
2979
+ const end = new Date(nowMs);
2980
+ end.setUTCHours(0, 0, 0, 0);
2981
+ const start = new Date(end);
2982
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2983
+ const activity = [];
2984
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2985
+ const date = cursor.toISOString().slice(0, 10);
2986
+ activity.push(
2987
+ days.get(date) ?? {
2988
+ date,
2989
+ privateConversations: 0,
2990
+ publicConversations: 0
2991
+ }
2992
+ );
2993
+ }
2994
+ return activity;
3270
2995
  }
3271
- function metadata(row) {
3272
- return {
3273
- id: row.id,
3274
- name: row.name,
3275
- tokenSuffix: row.tokenSuffix,
3276
- createdAt: row.createdAt.toISOString(),
3277
- expiresAt: row.expiresAt.toISOString(),
3278
- lastUsedAt: row.lastUsedAt?.toISOString() ?? null
2996
+ async function readLocationDirectoryFromSql() {
2997
+ const nowMs = Date.now();
2998
+ const end = new Date(nowMs);
2999
+ end.setUTCHours(0, 0, 0, 0);
3000
+ const start = new Date(end);
3001
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
3002
+ const [rows, activityRows] = await Promise.all([
3003
+ directoryRows(getDb()),
3004
+ directoryActivityRows(getDb(), start)
3005
+ ]);
3006
+ const locations = [];
3007
+ const privateActivity = {
3008
+ ...emptyMetrics(),
3009
+ label: "Private activity"
3279
3010
  };
3280
- }
3281
- async function createPersonalToken(args) {
3282
- const now = args.now ?? /* @__PURE__ */ new Date();
3283
- const token2 = `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
3284
- const row = {
3285
- id: randomUUID(),
3286
- ownerEmailNormalized: normalizeEmail2(args.email),
3287
- name: args.name.trim(),
3288
- tokenHash: hashToken(token2),
3289
- tokenSuffix: token2.slice(-TOKEN_SUFFIX_LENGTH),
3290
- createdAt: now,
3291
- expiresAt: new Date(now.getTime() + DEFAULT_TOKEN_LIFETIME_MS)
3011
+ for (const row of rows) {
3012
+ const location = locationFromAggregate(row);
3013
+ if (location) locations.push(location);
3014
+ else addMetrics(privateActivity, row);
3015
+ }
3016
+ return {
3017
+ activityDays: directoryActivityDays(activityRows, nowMs),
3018
+ generatedAt: new Date(nowMs).toISOString(),
3019
+ locations: locations.sort(
3020
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.label.localeCompare(right.label)
3021
+ ),
3022
+ privateActivity,
3023
+ source: "conversation_index",
3024
+ windowEnd: end.toISOString(),
3025
+ windowStart: start.toISOString()
3292
3026
  };
3293
- await getDb().insert(juniorApiTokens).values(row);
3294
- return { ...metadata({ ...row, lastUsedAt: null, revokedAt: null }), token: token2 };
3295
- }
3296
- async function listPersonalTokens(email, now = /* @__PURE__ */ new Date()) {
3297
- const rows = await getDb().select().from(juniorApiTokens).where(
3298
- and13(
3299
- eq14(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(email)),
3300
- isNull7(juniorApiTokens.revokedAt),
3301
- gt(juniorApiTokens.expiresAt, now)
3302
- )
3303
- ).orderBy(desc5(juniorApiTokens.createdAt));
3304
- return rows.map(metadata);
3305
- }
3306
- async function revokePersonalToken(args) {
3307
- const rows = await getDb().update(juniorApiTokens).set({ revokedAt: args.now ?? /* @__PURE__ */ new Date() }).where(
3308
- and13(
3309
- eq14(juniorApiTokens.id, args.id),
3310
- eq14(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(args.email)),
3311
- isNull7(juniorApiTokens.revokedAt)
3312
- )
3313
- ).returning({ id: juniorApiTokens.id });
3314
- return rows.length > 0;
3315
3027
  }
3316
- async function authenticatePersonalToken(token2, now = /* @__PURE__ */ new Date()) {
3317
- if (!token2.startsWith(TOKEN_PREFIX)) return void 0;
3318
- const rows = await getDb().select({
3319
- id: juniorApiTokens.id,
3320
- ownerEmailNormalized: juniorApiTokens.ownerEmailNormalized
3321
- }).from(juniorApiTokens).where(
3322
- and13(
3323
- eq14(juniorApiTokens.tokenHash, hashToken(token2)),
3324
- isNull7(juniorApiTokens.revokedAt),
3325
- gt(juniorApiTokens.expiresAt, now)
3028
+ async function recentLocationRows(db, locationId) {
3029
+ return db.select({
3030
+ channelName: juniorConversations.channelName,
3031
+ conversationId: juniorConversations.conversationId,
3032
+ createdAt: juniorConversations.createdAt,
3033
+ destinationId: juniorDestinations.id,
3034
+ durationMs: juniorConversations.durationMs,
3035
+ email: sql7`COALESCE(
3036
+ ${juniorUsers.primaryEmailNormalized},
3037
+ ${juniorIdentities.email}
3038
+ )`,
3039
+ executionStatus: juniorConversations.executionStatus,
3040
+ executionUpdatedAt: juniorConversations.executionUpdatedAt,
3041
+ fullName: juniorUsers.displayName,
3042
+ handle: juniorIdentities.handle,
3043
+ lastActivityAt: juniorConversations.lastActivityAt,
3044
+ providerSubjectId: sql7`CASE
3045
+ WHEN ${juniorIdentities.provider} = 'slack'
3046
+ THEN ${juniorIdentities.providerSubjectId}
3047
+ ELSE NULL
3048
+ END`,
3049
+ source: juniorConversations.source,
3050
+ title: juniorConversations.title,
3051
+ updatedAt: juniorConversations.updatedAt,
3052
+ usage: juniorConversations.usage
3053
+ }).from(juniorConversations).innerJoin(
3054
+ juniorDestinations,
3055
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3056
+ ).leftJoin(
3057
+ juniorIdentities,
3058
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
3059
+ ).leftJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(
3060
+ and9(
3061
+ publicLocationWhere(locationId),
3062
+ isNull5(juniorConversations.archivedAt)
3326
3063
  )
3327
- ).limit(1);
3328
- const match = rows[0];
3329
- if (!match) return void 0;
3330
- await getDb().update(juniorApiTokens).set({ lastUsedAt: now }).where(eq14(juniorApiTokens.id, match.id));
3331
- return match.ownerEmailNormalized;
3332
- }
3333
-
3334
- // src/api/personal-tokens/create.ts
3335
- var create_default = defineApiRoute({
3336
- method: "post",
3337
- path: "/",
3338
- responseSchema: createdPersonalTokenSchema,
3339
- handler: async (c) => {
3340
- const email = c.get("verifiedViewerEmail");
3341
- if (!email) throwApiError(403, "Verified viewer email required.");
3342
- let input;
3343
- try {
3344
- input = await c.req.json();
3345
- } catch (error) {
3346
- throwApiError(400, "Invalid request body.", error);
3347
- }
3348
- const body = parseBody(createPersonalTokenBodySchema, input);
3349
- return createPersonalToken({ email, name: body.name });
3350
- }
3351
- });
3352
-
3353
- // src/api/personal-tokens/list.ts
3354
- var list_default4 = defineApiRoute({
3355
- method: "get",
3356
- path: "/",
3357
- responseSchema: personalTokenListSchema,
3358
- handler: async (c) => {
3359
- const email = c.get("verifiedViewerEmail");
3360
- if (!email) throwApiError(403, "Verified viewer email required.");
3361
- return { tokens: await listPersonalTokens(email) };
3362
- }
3363
- });
3364
-
3365
- // src/api/personal-tokens/revoke.ts
3366
- var revoke_default = defineApiRoute({
3367
- method: "delete",
3368
- path: "/:id",
3369
- responseSchema: revokePersonalTokenResponseSchema,
3370
- handler: async (c) => {
3371
- const email = c.get("verifiedViewerEmail");
3372
- if (!email) throwApiError(403, "Verified viewer email required.");
3373
- const { id } = parseParams(personalTokenParamsSchema, c.req.param());
3374
- if (!await revokePersonalToken({ email, id })) {
3375
- throwApiError(404, "Personal API token not found.");
3376
- }
3377
- return { revoked: true };
3378
- }
3379
- });
3380
-
3381
- // src/api/personal-tokens/routes.ts
3382
- var routes3 = [list_default4, create_default, revoke_default];
3383
- function createPersonalTokenRoutes() {
3384
- const app = new Hono4();
3385
- registerApiRoutes(app, routes3);
3386
- return app;
3064
+ ).orderBy(
3065
+ desc3(juniorConversations.lastActivityAt),
3066
+ asc3(juniorConversations.conversationId)
3067
+ ).limit(RECENT_LIMIT);
3387
3068
  }
3388
-
3389
- // src/api/user-pages/routes.ts
3390
- import { Hono as Hono5 } from "hono";
3391
- import {
3392
- pluginUserPageContentSchema as pluginUserPageContentSchema2,
3393
- pluginUserPageInputSchema,
3394
- pluginUserPageLinksSchema as pluginUserPageLinksSchema2
3395
- } from "@sentry/junior-plugin-api";
3396
-
3397
- // src/chat/plugins/user-pages.ts
3398
- import {
3399
- pluginUserPageContentSchema,
3400
- pluginUserPageLinksSchema
3401
- } from "@sentry/junior-plugin-api";
3402
- function readPluginUserPageLinks() {
3403
- return pluginUserPageLinksSchema.parse(
3404
- getPlugins().flatMap(
3405
- (plugin) => (plugin.userPages ?? []).map((page) => ({
3406
- description: page.description,
3407
- id: page.id,
3408
- label: page.label,
3409
- navigation: page.navigation ?? "profile",
3410
- pluginDisplayName: plugin.manifest.displayName,
3411
- pluginName: plugin.manifest.name
3412
- }))
3413
- )
3414
- );
3069
+ async function readLocationDetailFromSql(locationId, options = {}) {
3070
+ const nowMs = Date.now();
3071
+ const end = new Date(nowMs);
3072
+ end.setUTCHours(0, 0, 0, 0);
3073
+ const start = new Date(end);
3074
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
3075
+ const where = publicLocationWhere(locationId);
3076
+ const activityDate = sql7`TO_CHAR(
3077
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3078
+ 'YYYY-MM-DD'
3079
+ )`;
3080
+ const [locationRows, dayRows, actorRows, recentRows] = await Promise.all([
3081
+ getDb().select({
3082
+ ...locationColumns(),
3083
+ ...treeAggregateColumns2,
3084
+ ...conversationRangeColumns()
3085
+ }).from(juniorConversations).innerJoin(
3086
+ treeConversation2,
3087
+ eq10(
3088
+ treeConversation2.rootConversationId,
3089
+ juniorConversations.conversationId
3090
+ )
3091
+ ).innerJoin(
3092
+ juniorDestinations,
3093
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3094
+ ).where(where).groupBy(...locationGroupBy()),
3095
+ getDb().select({
3096
+ date: activityDate,
3097
+ ...treeAggregateColumns2
3098
+ }).from(juniorConversations).innerJoin(
3099
+ treeConversation2,
3100
+ eq10(
3101
+ treeConversation2.rootConversationId,
3102
+ juniorConversations.conversationId
3103
+ )
3104
+ ).innerJoin(
3105
+ juniorDestinations,
3106
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3107
+ ).where(and9(where, gte2(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
3108
+ getDb().select({
3109
+ actorIdentityId: juniorConversations.actorIdentityId,
3110
+ email: juniorUsers.primaryEmailNormalized,
3111
+ fullName: juniorUsers.displayName,
3112
+ handle: juniorIdentities.handle,
3113
+ identityEmail: juniorIdentities.email,
3114
+ identityProvider: juniorIdentities.provider,
3115
+ providerSubjectId: juniorIdentities.providerSubjectId,
3116
+ ...treeAggregateColumns2
3117
+ }).from(juniorConversations).innerJoin(
3118
+ treeConversation2,
3119
+ eq10(
3120
+ treeConversation2.rootConversationId,
3121
+ juniorConversations.conversationId
3122
+ )
3123
+ ).innerJoin(
3124
+ juniorDestinations,
3125
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3126
+ ).leftJoin(
3127
+ juniorIdentities,
3128
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
3129
+ ).leftJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
3130
+ juniorConversations.actorIdentityId,
3131
+ juniorUsers.primaryEmailNormalized,
3132
+ juniorUsers.displayName,
3133
+ juniorIdentities.handle,
3134
+ juniorIdentities.email,
3135
+ juniorIdentities.provider,
3136
+ juniorIdentities.providerSubjectId
3137
+ ),
3138
+ recentLocationRows(getDb(), locationId)
3139
+ ]);
3140
+ const locationRow = locationRows[0];
3141
+ const location = locationRow ? locationFromAggregate(locationRow) : void 0;
3142
+ if (!location) return void 0;
3143
+ const days = /* @__PURE__ */ new Map();
3144
+ for (const row of dayRows) {
3145
+ days.set(row.date, {
3146
+ active: row.active,
3147
+ conversations: row.conversations,
3148
+ date: row.date,
3149
+ durationMs: row.durationMs,
3150
+ failed: row.failed,
3151
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3152
+ });
3153
+ }
3154
+ const actors = [];
3155
+ for (const row of actorRows) {
3156
+ if (!row.actorIdentityId) continue;
3157
+ const actor = {
3158
+ ...row.email ?? row.identityEmail ? { email: row.email ?? row.identityEmail ?? void 0 } : {},
3159
+ ...row.fullName ? { fullName: row.fullName } : {},
3160
+ ...row.identityProvider === "slack" && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
3161
+ ...row.handle ? { slackUserName: row.handle } : {}
3162
+ };
3163
+ if (!actor.email && !actor.slackUserId) continue;
3164
+ const item = emptyActor(actor);
3165
+ addMetrics(item, row);
3166
+ actors.push(item);
3167
+ }
3168
+ const activity = activityDays(days, nowMs, ACTIVITY_DAYS);
3169
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
3170
+ const [accessByConversation, metricsByRoot] = await Promise.all([
3171
+ readConversationAccessFromSql(
3172
+ getDb(),
3173
+ recentConversationIds,
3174
+ options.verifiedViewerEmail
3175
+ ),
3176
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
3177
+ ]);
3178
+ return {
3179
+ ...location,
3180
+ activityDays: activity,
3181
+ actors: actors.sort(
3182
+ (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
3183
+ ),
3184
+ generatedAt: new Date(nowMs).toISOString(),
3185
+ recentConversations: recentRows.map(
3186
+ (row) => summaryFromRow(row, {
3187
+ access: accessByConversation.get(row.conversationId),
3188
+ metrics: metricsByRoot.get(row.conversationId)
3189
+ })
3190
+ ),
3191
+ source: "conversation_index",
3192
+ windowEnd: end.toISOString(),
3193
+ windowStart: activity[0] ? `${activity[0].date}T00:00:00.000Z` : end.toISOString()
3194
+ };
3415
3195
  }
3416
- function actionBelongsToPlugin(href, pluginName) {
3417
- const parsed = new URL(href, "http://junior.local");
3418
- return parsed.origin === "http://junior.local" && parsed.pathname === href && parsed.pathname.startsWith(`/api/plugins/${pluginName}/`) && !parsed.search && !parsed.hash;
3196
+
3197
+ // src/api/locations/detail.ts
3198
+ async function readLocationDetail(locationId, options = {}) {
3199
+ const report = await readLocationDetailFromSql(locationId, options);
3200
+ return report ? locationDetailReportSchema.parse(report) : void 0;
3419
3201
  }
3420
- async function readPluginUserPage(input) {
3421
- const plugin = getPlugins().find(
3422
- (candidate) => candidate.manifest.name === input.pluginName
3423
- );
3424
- const page = plugin?.userPages?.find(
3425
- (candidate) => candidate.id === input.pageId
3426
- );
3427
- if (!plugin || !page) return void 0;
3428
- const viewer2 = await resolveViewerUser(input.email);
3429
- if (!viewer2) {
3430
- throw new Error("Authenticated viewer user could not be resolved");
3431
- }
3432
- const content = pluginUserPageContentSchema.parse(
3433
- await page.read(
3434
- {
3435
- db: getDb(),
3436
- log: createPluginLogger(plugin.manifest.name),
3437
- plugin: { name: plugin.manifest.name },
3438
- viewer: viewer2
3439
- },
3440
- input.query
3441
- )
3442
- );
3443
- if (content.records.some(
3444
- (record) => record.actions?.some(
3445
- (action) => !actionBelongsToPlugin(action.href, plugin.manifest.name)
3446
- )
3447
- )) {
3448
- throw new Error(
3449
- `Plugin user page "${plugin.manifest.name}/${page.id}" returned an action outside its API namespace.`
3202
+ var detail_default2 = defineApiRoute({
3203
+ method: "get",
3204
+ path: "/:locationId",
3205
+ responseSchema: locationDetailReportSchema,
3206
+ handler: async (c) => {
3207
+ const { locationId } = parseParams(locationParamsSchema, c.req.param());
3208
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
3209
+ const report = await readLocationDetail(
3210
+ locationId,
3211
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
3450
3212
  );
3213
+ if (!report) throwApiError(404, "Location not found.");
3214
+ return report;
3451
3215
  }
3452
- return content;
3453
- }
3216
+ });
3454
3217
 
3455
- // src/api/user-pages/routes.ts
3456
- function createUserPageRoutes() {
3457
- const app = new Hono5();
3458
- app.get(
3459
- "/",
3460
- () => jsonResponse(pluginUserPageLinksSchema2, readPluginUserPageLinks())
3218
+ // src/api/locations/list.ts
3219
+ async function readLocationDirectory() {
3220
+ return locationDirectoryReportSchema.parse(
3221
+ await readLocationDirectoryFromSql()
3461
3222
  );
3462
- app.get("/:pluginName/:pageId", async (context) => {
3463
- const email = context.get("verifiedViewerEmail")?.trim();
3464
- if (!email) {
3465
- return jsonResponse(
3466
- apiErrorSchema,
3467
- { error: "Authentication required." },
3468
- { status: 401 }
3469
- );
3470
- }
3471
- const pageInput = pluginUserPageInputSchema.safeParse({
3472
- cursor: context.req.query("cursor") || void 0,
3473
- filter: context.req.query("filter") || void 0,
3474
- limit: context.req.query("limit") ? Number(context.req.query("limit")) : 20,
3475
- query: context.req.query("q") || void 0
3476
- });
3477
- if (!pageInput.success) {
3478
- return jsonResponse(
3479
- apiErrorSchema,
3480
- { error: "Invalid user page query." },
3481
- { status: 400 }
3482
- );
3483
- }
3484
- const page = await readPluginUserPage({
3485
- email,
3486
- pageId: context.req.param("pageId"),
3487
- pluginName: context.req.param("pluginName"),
3488
- query: pageInput.data
3489
- });
3490
- return page ? jsonResponse(pluginUserPageContentSchema2, page) : jsonResponse(
3491
- apiErrorSchema,
3492
- { error: "User page not found." },
3493
- { status: 404 }
3494
- );
3495
- });
3223
+ }
3224
+ var list_default2 = defineApiRoute({
3225
+ method: "get",
3226
+ path: "/",
3227
+ responseSchema: locationDirectoryReportSchema,
3228
+ handler: readLocationDirectory
3229
+ });
3230
+
3231
+ // src/api/locations/routes.ts
3232
+ var routes2 = [list_default2, detail_default2];
3233
+ function createLocationRoutes() {
3234
+ const app = new Hono2();
3235
+ registerApiRoutes(app, routes2);
3496
3236
  return app;
3497
3237
  }
3498
3238
 
3499
- // src/api/tasks/routes.ts
3500
- import { Hono as Hono6 } from "hono";
3239
+ // src/api/people/routes.ts
3240
+ import { Hono as Hono3 } from "hono";
3501
3241
 
3502
- // src/chat/tasks/read.ts
3503
- import { and as and14, eq as eq15, or as or3 } from "drizzle-orm";
3242
+ // src/api/people/list.query.ts
3243
+ import { and as and11, eq as eq12, gte as gte3, sql as sql9 } from "drizzle-orm";
3504
3244
 
3505
- // src/chat/scheduled-tasks/personal.ts
3506
- import { z as z3 } from "zod";
3507
- var cursorSchema = z3.object({
3508
- createdAtMs: z3.number().finite(),
3509
- id: z3.string().min(1),
3510
- query: z3.string().max(200).optional(),
3511
- version: z3.literal(1)
3512
- }).strict();
3513
- var InvalidScheduledTaskCursorError = class extends Error {
3514
- constructor() {
3515
- super("Scheduled task cursor is invalid.");
3516
- this.name = "InvalidScheduledTaskCursorError";
3517
- }
3518
- };
3519
- var PersonalScheduledTaskNotFoundError = class extends Error {
3520
- constructor() {
3521
- super("Scheduled task was not found.");
3522
- this.name = "PersonalScheduledTaskNotFoundError";
3245
+ // src/api/people/shared.ts
3246
+ import { and as and10, asc as asc4, desc as desc4, eq as eq11, isNull as isNull6, notExists, or as or3, sql as sql8 } from "drizzle-orm";
3247
+ import { alias as alias6 } from "drizzle-orm/pg-core";
3248
+ var RECENT_LIMIT2 = 25;
3249
+ var ACTIVITY_DAYS2 = 365;
3250
+ var peopleTreeConversation = alias6(
3251
+ juniorConversations,
3252
+ "people_tree_conversation"
3253
+ );
3254
+ var peopleRootConversation = alias6(
3255
+ juniorConversations,
3256
+ "people_root_conversation"
3257
+ );
3258
+ var peopleRootIdentity = alias6(juniorIdentities, "people_root_identity");
3259
+ var peopleTreeAggregateColumns = conversationAggregateColumns({
3260
+ metrics: peopleTreeConversation,
3261
+ roots: juniorConversations
3262
+ });
3263
+ function peopleTreeMetricsJoin() {
3264
+ const rootOwnedByActor = getDb().select({ conversationId: peopleRootConversation.conversationId }).from(peopleRootConversation).innerJoin(
3265
+ peopleRootIdentity,
3266
+ eq11(peopleRootIdentity.id, peopleRootConversation.actorIdentityId)
3267
+ ).where(
3268
+ and10(
3269
+ eq11(
3270
+ peopleRootConversation.conversationId,
3271
+ juniorConversations.rootConversationId
3272
+ ),
3273
+ isNull6(peopleRootConversation.parentConversationId),
3274
+ eq11(
3275
+ peopleRootConversation.rootConversationId,
3276
+ peopleRootConversation.conversationId
3277
+ ),
3278
+ eq11(
3279
+ peopleRootIdentity.emailNormalized,
3280
+ juniorUsers.primaryEmailNormalized
3281
+ ),
3282
+ eq11(peopleRootIdentity.emailVerified, true)
3283
+ )
3284
+ );
3285
+ return and10(
3286
+ eq11(
3287
+ peopleTreeConversation.rootConversationId,
3288
+ juniorConversations.rootConversationId
3289
+ ),
3290
+ or3(
3291
+ eq11(
3292
+ juniorConversations.rootConversationId,
3293
+ juniorConversations.conversationId
3294
+ ),
3295
+ and10(
3296
+ eq11(
3297
+ peopleTreeConversation.conversationId,
3298
+ juniorConversations.conversationId
3299
+ ),
3300
+ notExists(rootOwnedByActor)
3301
+ )
3302
+ )
3303
+ );
3304
+ }
3305
+ function normalizeEmail(email) {
3306
+ const normalized = email?.trim().toLowerCase();
3307
+ return normalized || void 0;
3308
+ }
3309
+ function emptyTotals() {
3310
+ return {
3311
+ active: 0,
3312
+ activeDays: 0,
3313
+ conversations: 0,
3314
+ durationMs: 0,
3315
+ failed: 0
3316
+ };
3317
+ }
3318
+ function emptyActivityDay(date) {
3319
+ return {
3320
+ active: 0,
3321
+ conversations: 0,
3322
+ date,
3323
+ durationMs: 0,
3324
+ failed: 0
3325
+ };
3326
+ }
3327
+ function activityDays2(days, nowMs) {
3328
+ const items = [];
3329
+ const end = new Date(nowMs);
3330
+ end.setUTCHours(0, 0, 0, 0);
3331
+ const start = new Date(end);
3332
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3333
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
3334
+ const date = cursor.toISOString().slice(0, 10);
3335
+ items.push(days.get(date) ?? emptyActivityDay(date));
3523
3336
  }
3524
- };
3525
- function normalizeQuery(query) {
3526
- return query?.trim().toLowerCase() || void 0;
3337
+ return items;
3527
3338
  }
3528
- function decodeCursor(value, query) {
3529
- if (!value) return void 0;
3530
- try {
3531
- const parsed = cursorSchema.parse(
3532
- JSON.parse(Buffer.from(value, "base64url").toString("utf8"))
3339
+ function statsItems2(map) {
3340
+ return [...map.values()].sort(
3341
+ (left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
3342
+ );
3343
+ }
3344
+ function verifiedActorWhere(email) {
3345
+ const normalizedEmail = normalizeEmail(email);
3346
+ return and10(
3347
+ eq11(juniorIdentities.provider, "slack"),
3348
+ eq11(juniorIdentities.emailVerified, true),
3349
+ sql8`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
3350
+ normalizedEmail ? eq11(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0
3351
+ );
3352
+ }
3353
+ async function recentActorRows(email) {
3354
+ return getDb().select({
3355
+ channelName: juniorConversations.channelName,
3356
+ conversationId: juniorConversations.conversationId,
3357
+ createdAt: juniorConversations.createdAt,
3358
+ destinationId: juniorDestinations.id,
3359
+ durationMs: juniorConversations.durationMs,
3360
+ email: juniorUsers.primaryEmailNormalized,
3361
+ executionStatus: juniorConversations.executionStatus,
3362
+ executionUpdatedAt: juniorConversations.executionUpdatedAt,
3363
+ fullName: juniorUsers.displayName,
3364
+ handle: juniorIdentities.handle,
3365
+ lastActivityAt: juniorConversations.lastActivityAt,
3366
+ providerSubjectId: juniorIdentities.providerSubjectId,
3367
+ source: juniorConversations.source,
3368
+ title: juniorConversations.title,
3369
+ updatedAt: juniorConversations.updatedAt,
3370
+ usage: juniorConversations.usage
3371
+ }).from(juniorConversations).innerJoin(
3372
+ juniorIdentities,
3373
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
3374
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(
3375
+ juniorDestinations,
3376
+ eq11(juniorDestinations.id, juniorConversations.destinationId)
3377
+ ).where(
3378
+ and10(verifiedActorWhere(email), isNull6(juniorConversations.archivedAt))
3379
+ ).orderBy(
3380
+ desc4(juniorConversations.lastActivityAt),
3381
+ asc4(juniorConversations.conversationId)
3382
+ ).limit(RECENT_LIMIT2);
3383
+ }
3384
+
3385
+ // src/api/people/list.query.ts
3386
+ var DIRECTORY_ACTIVITY_DAYS = 90;
3387
+ function activityWindow(nowMs) {
3388
+ const end = new Date(nowMs);
3389
+ end.setUTCHours(0, 0, 0, 0);
3390
+ const start = new Date(end);
3391
+ start.setUTCDate(start.getUTCDate() - (DIRECTORY_ACTIVITY_DAYS - 1));
3392
+ return { end, start };
3393
+ }
3394
+ function directoryActivityDays2(rows, nowMs) {
3395
+ const days = new Map(rows.map((row) => [row.date, row]));
3396
+ const { end, start } = activityWindow(nowMs);
3397
+ const items = [];
3398
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
3399
+ const date = cursor.toISOString().slice(0, 10);
3400
+ items.push(days.get(date) ?? { activePeople: 0, conversations: 0, date });
3401
+ }
3402
+ return items;
3403
+ }
3404
+ async function readPeopleListFromSql() {
3405
+ const nowMs = Date.now();
3406
+ const { end, start } = activityWindow(nowMs);
3407
+ const activityDate = sql9`TO_CHAR(
3408
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3409
+ 'YYYY-MM-DD'
3410
+ )`;
3411
+ const [rows, activityRows] = await Promise.all([
3412
+ getDb().select({
3413
+ email: juniorUsers.primaryEmailNormalized,
3414
+ fullName: juniorUsers.displayName,
3415
+ slackUserId: sql9`MAX(${juniorIdentities.providerSubjectId})`,
3416
+ slackUserName: sql9`MAX(${juniorIdentities.handle})`,
3417
+ activeDays: conversationActiveDaysColumn(),
3418
+ ...peopleTreeAggregateColumns,
3419
+ ...conversationRangeColumns()
3420
+ }).from(juniorConversations).innerJoin(
3421
+ juniorIdentities,
3422
+ eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3423
+ ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, peopleTreeMetricsJoin()).where(verifiedActorWhere()).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
3424
+ getDb().select({
3425
+ activePeople: sql9`COUNT(DISTINCT ${juniorUsers.id})::int`,
3426
+ conversations: sql9`COUNT(*)::int`,
3427
+ date: activityDate
3428
+ }).from(juniorConversations).innerJoin(
3429
+ juniorIdentities,
3430
+ eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3431
+ ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).where(
3432
+ and11(
3433
+ verifiedActorWhere(),
3434
+ gte3(juniorConversations.lastActivityAt, start)
3435
+ )
3436
+ ).groupBy(activityDate)
3437
+ ]);
3438
+ const people = rows.map((row) => {
3439
+ const actor = {
3440
+ email: row.email,
3441
+ ...row.fullName ? { fullName: row.fullName } : {},
3442
+ ...row.slackUserId ? { slackUserId: row.slackUserId } : {},
3443
+ ...row.slackUserName ? { slackUserName: row.slackUserName } : {}
3444
+ };
3445
+ return {
3446
+ active: row.active,
3447
+ activeDays: row.activeDays,
3448
+ conversations: row.conversations,
3449
+ durationMs: row.durationMs,
3450
+ failed: row.failed,
3451
+ firstSeenAt: row.firstSeenAt.toISOString(),
3452
+ lastSeenAt: row.lastSeenAt.toISOString(),
3453
+ actor,
3454
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3455
+ };
3456
+ });
3457
+ return {
3458
+ activityDays: directoryActivityDays2(activityRows, nowMs),
3459
+ generatedAt: new Date(nowMs).toISOString(),
3460
+ people: people.sort(
3461
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.actor.email.localeCompare(right.actor.email)
3462
+ ),
3463
+ source: "conversation_index",
3464
+ windowEnd: end.toISOString(),
3465
+ windowStart: start.toISOString()
3466
+ };
3467
+ }
3468
+
3469
+ // src/api/people/list.ts
3470
+ async function readPeopleList() {
3471
+ return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
3472
+ }
3473
+ var list_default3 = defineApiRoute({
3474
+ method: "get",
3475
+ path: "/",
3476
+ responseSchema: actorDirectoryReportSchema,
3477
+ handler: readPeopleList
3478
+ });
3479
+
3480
+ // src/api/people/profile.query.ts
3481
+ import { and as and12, eq as eq13, gte as gte4, sql as sql10 } from "drizzle-orm";
3482
+ function emptyProfile(email, nowMs) {
3483
+ const end = new Date(nowMs);
3484
+ end.setUTCHours(0, 0, 0, 0);
3485
+ const start = new Date(end);
3486
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3487
+ return {
3488
+ activityDays: activityDays2(/* @__PURE__ */ new Map(), nowMs),
3489
+ generatedAt: new Date(nowMs).toISOString(),
3490
+ locations: [],
3491
+ recentConversations: [],
3492
+ actor: { email },
3493
+ source: "conversation_index",
3494
+ surfaces: [],
3495
+ totals: emptyTotals(),
3496
+ windowEnd: end.toISOString(),
3497
+ windowStart: start.toISOString()
3498
+ };
3499
+ }
3500
+ function addAggregate2(map, label, row) {
3501
+ const item = map.get(label) ?? {
3502
+ active: 0,
3503
+ conversations: 0,
3504
+ durationMs: 0,
3505
+ failed: 0,
3506
+ label
3507
+ };
3508
+ item.active += row.active;
3509
+ item.conversations += row.conversations;
3510
+ item.durationMs += row.durationMs;
3511
+ item.failed += row.failed;
3512
+ if (row.tokens !== null) item.tokens = (item.tokens ?? 0) + row.tokens;
3513
+ map.set(label, item);
3514
+ }
3515
+ function surfaceExpression() {
3516
+ return sql10`CASE
3517
+ WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
3518
+ THEN ${juniorConversations.source}
3519
+ WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
3520
+ WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
3521
+ WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
3522
+ ELSE 'internal'
3523
+ END`;
3524
+ }
3525
+ function locationLabel2(row) {
3526
+ if (row.surface !== "slack")
3527
+ return surfaceLabel2(row.surface);
3528
+ if (row.destinationVisibility !== "public") return "Private Conversation";
3529
+ return slackLocationLabel({
3530
+ channel: row.channel || void 0,
3531
+ channelName: row.channelName ?? void 0
3532
+ }) ?? "Conversation";
3533
+ }
3534
+ async function readPeopleProfileFromSql(email, options = {}) {
3535
+ const nowMs = Date.now();
3536
+ const normalizedEmail = normalizeEmail(email);
3537
+ if (!normalizedEmail) return emptyProfile("", nowMs);
3538
+ const end = new Date(nowMs);
3539
+ end.setUTCHours(0, 0, 0, 0);
3540
+ const start = new Date(end);
3541
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3542
+ const where = verifiedActorWhere(normalizedEmail);
3543
+ const metricsJoin = peopleTreeMetricsJoin();
3544
+ const surface = surfaceExpression();
3545
+ const activityDate = sql10`TO_CHAR(
3546
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3547
+ 'YYYY-MM-DD'
3548
+ )`;
3549
+ const channel = sql10`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
3550
+ const [totalsRows, dayRows, locationRows, surfaceRows, recentRows] = await Promise.all([
3551
+ getDb().select({
3552
+ email: juniorUsers.primaryEmailNormalized,
3553
+ fullName: juniorUsers.displayName,
3554
+ slackUserId: sql10`MAX(${juniorIdentities.providerSubjectId})`,
3555
+ slackUserName: sql10`MAX(${juniorIdentities.handle})`,
3556
+ activeDays: conversationActiveDaysColumn(),
3557
+ ...peopleTreeAggregateColumns
3558
+ }).from(juniorConversations).innerJoin(
3559
+ juniorIdentities,
3560
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3561
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
3562
+ getDb().select({
3563
+ date: activityDate,
3564
+ ...peopleTreeAggregateColumns
3565
+ }).from(juniorConversations).innerJoin(
3566
+ juniorIdentities,
3567
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3568
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(and12(where, gte4(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
3569
+ getDb().select({
3570
+ channel,
3571
+ channelName: juniorConversations.channelName,
3572
+ destinationVisibility: juniorDestinations.visibility,
3573
+ surface,
3574
+ ...peopleTreeAggregateColumns
3575
+ }).from(juniorConversations).innerJoin(
3576
+ juniorIdentities,
3577
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3578
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).leftJoin(
3579
+ juniorDestinations,
3580
+ eq13(juniorDestinations.id, juniorConversations.destinationId)
3581
+ ).where(where).groupBy(
3582
+ channel,
3583
+ juniorConversations.channelName,
3584
+ juniorDestinations.visibility,
3585
+ surface
3586
+ ),
3587
+ getDb().select({ surface, ...peopleTreeAggregateColumns }).from(juniorConversations).innerJoin(
3588
+ juniorIdentities,
3589
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3590
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(surface),
3591
+ recentActorRows(normalizedEmail)
3592
+ ]);
3593
+ const totalsRow = totalsRows[0];
3594
+ if (!totalsRow) return emptyProfile(normalizedEmail, nowMs);
3595
+ const actor = {
3596
+ email: totalsRow.email,
3597
+ ...totalsRow.fullName ? { fullName: totalsRow.fullName } : {},
3598
+ ...totalsRow.slackUserId ? { slackUserId: totalsRow.slackUserId } : {},
3599
+ ...totalsRow.slackUserName ? { slackUserName: totalsRow.slackUserName } : {}
3600
+ };
3601
+ const days = /* @__PURE__ */ new Map();
3602
+ for (const row of dayRows) {
3603
+ days.set(row.date, {
3604
+ active: row.active,
3605
+ conversations: row.conversations,
3606
+ ...row.costUsd !== null ? { costUsd: row.costUsd } : {},
3607
+ date: row.date,
3608
+ durationMs: row.durationMs,
3609
+ failed: row.failed,
3610
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3611
+ });
3612
+ }
3613
+ const locations = /* @__PURE__ */ new Map();
3614
+ for (const row of locationRows) {
3615
+ addAggregate2(locations, locationLabel2(row), row);
3616
+ }
3617
+ const surfaces = /* @__PURE__ */ new Map();
3618
+ for (const row of surfaceRows) {
3619
+ addAggregate2(
3620
+ surfaces,
3621
+ surfaceLabel2(row.surface),
3622
+ row
3533
3623
  );
3534
- if (parsed.query !== query) {
3535
- throw new InvalidScheduledTaskCursorError();
3536
- }
3537
- return { createdAtMs: parsed.createdAtMs, id: parsed.id };
3538
- } catch {
3539
- throw new InvalidScheduledTaskCursorError();
3540
3624
  }
3541
- }
3542
- function encodeCursor(task, query) {
3543
- return Buffer.from(
3544
- JSON.stringify({
3545
- createdAtMs: task.createdAtMs,
3546
- id: task.id,
3547
- ...query ? { query } : {},
3548
- version: 1
3549
- }),
3550
- "utf8"
3551
- ).toString("base64url");
3552
- }
3553
- function createViewerScheduledTasks(store, user) {
3554
- const identityIds = new Set(user.identities.map((identity) => identity.id));
3625
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
3626
+ const [accessByConversation, metricsByRoot] = await Promise.all([
3627
+ readConversationAccessFromSql(
3628
+ getDb(),
3629
+ recentConversationIds,
3630
+ options.verifiedViewerEmail
3631
+ ),
3632
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
3633
+ ]);
3555
3634
  return {
3556
- async delete(id, nowMs = Date.now()) {
3557
- const task = await store.getTask(id);
3558
- if (!task || task.status === "deleted" || !identityIds.has(task.creatorIdentityId)) {
3559
- throw new PersonalScheduledTaskNotFoundError();
3560
- }
3561
- await store.saveTask({
3562
- ...task,
3563
- nextRunAtMs: void 0,
3564
- runNowAtMs: void 0,
3565
- status: "deleted",
3566
- updatedAtMs: nowMs
3567
- });
3635
+ activityDays: activityDays2(days, nowMs),
3636
+ generatedAt: new Date(nowMs).toISOString(),
3637
+ locations: statsItems2(locations),
3638
+ recentConversations: recentRows.map(
3639
+ (row) => summaryFromRow(row, {
3640
+ access: accessByConversation.get(row.conversationId),
3641
+ metrics: metricsByRoot.get(row.conversationId)
3642
+ })
3643
+ ),
3644
+ actor,
3645
+ source: "conversation_index",
3646
+ surfaces: statsItems2(surfaces),
3647
+ totals: {
3648
+ active: totalsRow.active,
3649
+ activeDays: totalsRow.activeDays,
3650
+ conversations: totalsRow.conversations,
3651
+ durationMs: totalsRow.durationMs,
3652
+ failed: totalsRow.failed,
3653
+ ...totalsRow.tokens !== null ? { tokens: totalsRow.tokens } : {}
3568
3654
  },
3569
- async list(input) {
3570
- const query = normalizeQuery(input.query);
3571
- const cursor = decodeCursor(input.cursor, query);
3572
- const matching = await store.listTasksCreatedBy({
3573
- ...cursor ? { before: cursor } : {},
3574
- identityIds: [...identityIds],
3575
- limit: input.limit + 1,
3576
- ...query ? { query } : {}
3577
- });
3578
- const tasks = matching.slice(0, input.limit);
3579
- return {
3580
- tasks,
3581
- ...matching.length > input.limit && tasks.length > 0 ? { nextCursor: encodeCursor(tasks.at(-1), query) } : {}
3582
- };
3583
- }
3655
+ windowEnd: end.toISOString(),
3656
+ windowStart: start.toISOString()
3584
3657
  };
3585
3658
  }
3586
3659
 
3587
- // src/chat/tasks/read.ts
3588
- var TASK_LIST_LIMIT = 100;
3589
- var TASK_FETCH_LIMIT = TASK_LIST_LIMIT + 1;
3590
- var TASK_EXECUTION_LIST_LIMIT = 100;
3591
- function creatorLabel(creator) {
3592
- return creator.fullName?.trim() || (creator.userName?.trim() ? `@${creator.userName.trim()}` : "") || creator.slackUserId;
3660
+ // src/api/people/profile.ts
3661
+ async function readPeopleProfile(email, options = {}) {
3662
+ return actorProfileReportSchema.parse(
3663
+ await readPeopleProfileFromSql(email, options)
3664
+ );
3593
3665
  }
3594
- function creatorKey(teamId, slackUserId) {
3595
- return `${teamId}:${slackUserId}`;
3666
+ var profile_default = defineApiRoute({
3667
+ method: "get",
3668
+ path: "/:email",
3669
+ responseSchema: actorProfileReportSchema,
3670
+ handler: async (c) => {
3671
+ const { email } = parseParams(personParamsSchema, c.req.param());
3672
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
3673
+ return readPeopleProfile(
3674
+ email,
3675
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
3676
+ );
3677
+ }
3678
+ });
3679
+
3680
+ // src/api/people/spend.query.ts
3681
+ import { and as and13, eq as eq14, gte as gte5, lte as lte2, sql as sql11 } from "drizzle-orm";
3682
+ var SEVEN_DAY_OFFSET = 6;
3683
+ var THIRTY_DAY_OFFSET = 29;
3684
+ function addUsd2(current, next) {
3685
+ return Math.round((current + next) * 1e12) / 1e12;
3596
3686
  }
3597
- async function creatorProfileEmails(candidates) {
3598
- const selectors = new Map(
3599
- candidates.map(({ task }) => {
3600
- const teamId = task.destination.teamId;
3601
- const slackUserId = task.createdBy.slackUserId;
3602
- return [
3603
- creatorKey(teamId, slackUserId),
3604
- { slackUserId, teamId }
3605
- ];
3606
- })
3607
- );
3608
- if (selectors.size === 0) return /* @__PURE__ */ new Map();
3609
- const rows = await getDb().select({
3610
- email: juniorUsers.primaryEmailNormalized,
3611
- slackUserId: juniorIdentities.providerSubjectId,
3612
- teamId: juniorIdentities.providerTenantId
3613
- }).from(juniorIdentities).innerJoin(juniorUsers, eq15(juniorUsers.id, juniorIdentities.userId)).where(
3614
- and14(
3615
- eq15(juniorIdentities.kind, "user"),
3616
- eq15(juniorIdentities.provider, "slack"),
3617
- eq15(juniorIdentities.emailVerified, true),
3618
- or3(
3619
- ...[...selectors.values()].map(
3620
- (selector) => and14(
3621
- eq15(juniorIdentities.providerTenantId, selector.teamId),
3622
- eq15(juniorIdentities.providerSubjectId, selector.slackUserId)
3623
- )
3624
- )
3625
- )
3687
+ function spendWindow(nowMs) {
3688
+ const end = new Date(nowMs);
3689
+ const currentDay = new Date(end);
3690
+ currentDay.setUTCHours(0, 0, 0, 0);
3691
+ const sevenDayStart = new Date(currentDay);
3692
+ sevenDayStart.setUTCDate(sevenDayStart.getUTCDate() - SEVEN_DAY_OFFSET);
3693
+ const thirtyDayStart = new Date(currentDay);
3694
+ thirtyDayStart.setUTCDate(thirtyDayStart.getUTCDate() - THIRTY_DAY_OFFSET);
3695
+ return { end, sevenDayStart, thirtyDayStart };
3696
+ }
3697
+ async function readPersonalSpendFromSql(email, nowMs = Date.now()) {
3698
+ const normalizedEmail = normalizeEmail(email);
3699
+ const { end, sevenDayStart, thirtyDayStart } = spendWindow(nowMs);
3700
+ const activityDate = sql11`TO_CHAR(
3701
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3702
+ 'YYYY-MM-DD'
3703
+ )`;
3704
+ const metricsJoin = peopleTreeMetricsJoin();
3705
+ const rows = normalizedEmail ? await getDb().select({
3706
+ costUsd: peopleTreeAggregateColumns.costUsd,
3707
+ date: activityDate
3708
+ }).from(juniorConversations).innerJoin(
3709
+ juniorIdentities,
3710
+ eq14(juniorIdentities.id, juniorConversations.actorIdentityId)
3711
+ ).innerJoin(juniorUsers, eq14(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(
3712
+ and13(
3713
+ verifiedActorWhere(normalizedEmail),
3714
+ gte5(juniorConversations.lastActivityAt, thirtyDayStart),
3715
+ lte2(juniorConversations.lastActivityAt, end)
3626
3716
  )
3627
- );
3628
- return new Map(
3629
- rows.map((row) => [creatorKey(row.teamId, row.slackUserId), row.email])
3630
- );
3717
+ ).groupBy(activityDate) : [];
3718
+ const sevenDayStartDate = sevenDayStart.toISOString().slice(0, 10);
3719
+ let sevenDaysUsd = 0;
3720
+ let thirtyDaysUsd = 0;
3721
+ for (const row of rows) {
3722
+ if (row.costUsd === null) continue;
3723
+ thirtyDaysUsd = addUsd2(thirtyDaysUsd, row.costUsd);
3724
+ if (row.date >= sevenDayStartDate) {
3725
+ sevenDaysUsd = addUsd2(sevenDaysUsd, row.costUsd);
3726
+ }
3727
+ }
3728
+ return {
3729
+ generatedAt: new Date(nowMs).toISOString(),
3730
+ sevenDaysUsd,
3731
+ source: "conversation_index",
3732
+ thirtyDaysUsd,
3733
+ windowEnd: end.toISOString(),
3734
+ windowStart: thirtyDayStart.toISOString()
3735
+ };
3631
3736
  }
3632
- function destinationKey(destination) {
3633
- return `${destination.teamId}:${destination.channelId}`;
3737
+
3738
+ // src/api/people/spend.ts
3739
+ var PERSONAL_SPEND_CACHE_TTL_MS = 5 * 6e4;
3740
+ function createPersonalSpendRoute() {
3741
+ const cache = /* @__PURE__ */ new Map();
3742
+ async function read(email) {
3743
+ const normalizedEmail = email.trim().toLowerCase();
3744
+ const nowMs = Date.now();
3745
+ const cached = cache.get(normalizedEmail);
3746
+ if (cached && cached.expiresAtMs > nowMs) {
3747
+ return cached.report;
3748
+ }
3749
+ cache.delete(normalizedEmail);
3750
+ const report = readPersonalSpendFromSql(normalizedEmail, nowMs);
3751
+ const entry = {
3752
+ expiresAtMs: nowMs + PERSONAL_SPEND_CACHE_TTL_MS,
3753
+ report
3754
+ };
3755
+ cache.set(normalizedEmail, entry);
3756
+ try {
3757
+ return await report;
3758
+ } catch (error) {
3759
+ if (cache.get(normalizedEmail) === entry) cache.delete(normalizedEmail);
3760
+ throw error;
3761
+ }
3762
+ }
3763
+ return defineApiRoute({
3764
+ method: "get",
3765
+ path: "/me/spend",
3766
+ responseSchema: personalSpendReportSchema,
3767
+ handler: async (context) => {
3768
+ const email = context.get("verifiedViewerEmail");
3769
+ if (!email) throwApiError(403, "Verified viewer email required.");
3770
+ return read(email);
3771
+ }
3772
+ });
3634
3773
  }
3635
- function displayText(value, fallback) {
3636
- return value.trim() || fallback;
3774
+
3775
+ // src/api/people/routes.ts
3776
+ function createPeopleRoutes() {
3777
+ const app = new Hono3();
3778
+ const routes5 = [
3779
+ createPersonalSpendRoute(),
3780
+ list_default3,
3781
+ profile_default
3782
+ ];
3783
+ registerApiRoutes(app, routes5);
3784
+ return app;
3637
3785
  }
3638
- function taskDisplayTitle(title, instruction, fallback) {
3639
- const stored = title?.trim();
3640
- if (stored) return stored;
3641
- return fallbackShortTitle(instruction, fallback);
3786
+
3787
+ // src/api/plugin-reports.ts
3788
+ var plugin_reports_default = defineApiRoute({
3789
+ method: "get",
3790
+ path: "/api/plugin-reports",
3791
+ responseSchema: pluginOperationalReportFeedSchema,
3792
+ handler: readPluginOperationalReportFeed
3793
+ });
3794
+
3795
+ // src/api/personal-tokens/routes.ts
3796
+ import { Hono as Hono4 } from "hono";
3797
+
3798
+ // src/personal-tokens/store.ts
3799
+ import { createHash, randomBytes, randomUUID } from "crypto";
3800
+ import { and as and14, desc as desc5, eq as eq15, gt, isNull as isNull7 } from "drizzle-orm";
3801
+ var TOKEN_PREFIX = "jr_pat_";
3802
+ var TOKEN_SUFFIX_LENGTH = 8;
3803
+ var DEFAULT_TOKEN_LIFETIME_MS = 90 * 24 * 60 * 60 * 1e3;
3804
+ function normalizeEmail2(email) {
3805
+ return email.trim().toLowerCase();
3642
3806
  }
3643
- async function destinationDetails(destinations) {
3644
- const selectors = new Map(
3645
- destinations.map((destination) => [
3646
- destinationKey(destination),
3647
- destination
3648
- ])
3649
- );
3650
- if (selectors.size === 0) return /* @__PURE__ */ new Map();
3651
- const rows = await getDb().select({
3652
- displayName: juniorDestinations.displayName,
3653
- kind: juniorDestinations.kind,
3654
- providerDestinationId: juniorDestinations.providerDestinationId,
3655
- providerTenantId: juniorDestinations.providerTenantId,
3656
- visibility: juniorDestinations.visibility
3657
- }).from(juniorDestinations).where(
3658
- or3(
3659
- ...[...selectors.values()].map(
3660
- (destination) => and14(
3661
- eq15(juniorDestinations.provider, "slack"),
3662
- eq15(juniorDestinations.providerTenantId, destination.teamId),
3663
- eq15(juniorDestinations.providerDestinationId, destination.channelId)
3664
- )
3665
- )
3666
- )
3667
- );
3668
- return new Map(
3669
- rows.map((row) => {
3670
- const name = row.displayName?.trim();
3671
- const label = name ? row.kind === "channel" ? `#${name.replace(/^#/, "")}` : name : row.kind === "dm" ? "Direct message" : row.kind === "group" ? "Group message" : `Channel ${row.providerDestinationId}`;
3672
- return [
3673
- `${row.providerTenantId}:${row.providerDestinationId}`,
3674
- {
3675
- label,
3676
- visibility: row.visibility === "public" ? "public" : "private"
3677
- }
3678
- ];
3679
- })
3680
- );
3807
+ function hashToken(token2) {
3808
+ return createHash("sha256").update(token2, "utf8").digest("hex");
3681
3809
  }
3682
- function executionSummaryFields(stats) {
3810
+ function metadata(row) {
3683
3811
  return {
3684
- ...stats?.lastConversationId ? { lastConversationId: stats.lastConversationId } : {},
3685
- ...stats?.lastExecutedAtMs ? { lastRunAt: new Date(stats.lastExecutedAtMs).toISOString() } : {},
3686
- runsLast7Days: stats?.runsLast7Days ?? 0,
3687
- totalRuns: stats?.totalRuns ?? 0
3812
+ id: row.id,
3813
+ name: row.name,
3814
+ tokenSuffix: row.tokenSuffix,
3815
+ createdAt: row.createdAt.toISOString(),
3816
+ expiresAt: row.expiresAt.toISOString(),
3817
+ lastUsedAt: row.lastUsedAt?.toISOString() ?? null
3688
3818
  };
3689
3819
  }
3690
- function scheduledTaskSummary(task, ownedByViewer, destination, stats, createdByEmail) {
3691
- if (task.status === "deleted") {
3692
- throw new Error("Deleted scheduled tasks cannot enter the Tasks view");
3693
- }
3694
- const nextRunAtMs = task.runNowAtMs ?? task.nextRunAtMs;
3695
- const instruction = displayText(task.task.text, "Untitled scheduled task");
3696
- return {
3697
- createdAt: new Date(task.createdAtMs).toISOString(),
3698
- createdBy: creatorLabel(task.createdBy),
3699
- ...createdByEmail ? { createdByEmail } : {},
3700
- destination: {
3701
- channelId: task.destination.channelId,
3702
- label: destination.label,
3703
- teamId: task.destination.teamId,
3704
- visibility: destination.visibility
3705
- },
3706
- id: task.id,
3707
- instruction,
3708
- kind: "scheduled",
3709
- ...executionSummaryFields(stats),
3710
- ...nextRunAtMs !== void 0 ? { nextRunAt: new Date(nextRunAtMs).toISOString() } : {},
3711
- ownedByViewer,
3712
- schedule: displayText(task.schedule.description, "Schedule unavailable"),
3713
- status: task.status,
3714
- title: taskDisplayTitle(task.title, instruction, "Untitled scheduled task")
3820
+ async function createPersonalToken(args) {
3821
+ const now = args.now ?? /* @__PURE__ */ new Date();
3822
+ const token2 = `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
3823
+ const row = {
3824
+ id: randomUUID(),
3825
+ ownerEmailNormalized: normalizeEmail2(args.email),
3826
+ name: args.name.trim(),
3827
+ tokenHash: hashToken(token2),
3828
+ tokenSuffix: token2.slice(-TOKEN_SUFFIX_LENGTH),
3829
+ createdAt: now,
3830
+ expiresAt: new Date(now.getTime() + DEFAULT_TOKEN_LIFETIME_MS)
3715
3831
  };
3832
+ await getDb().insert(juniorApiTokens).values(row);
3833
+ return { ...metadata({ ...row, lastUsedAt: null, revokedAt: null }), token: token2 };
3716
3834
  }
3717
- function eventTaskSummary(task, ownedByViewer, destination, stats, createdByEmail) {
3718
- const instruction = task.task.text;
3719
- return {
3720
- createdAt: new Date(task.createdAtMs).toISOString(),
3721
- createdBy: creatorLabel(task.createdBy),
3722
- ...createdByEmail ? { createdByEmail } : {},
3723
- destination: {
3724
- channelId: task.destination.channelId,
3725
- label: destination.label,
3726
- teamId: task.destination.teamId,
3727
- visibility: destination.visibility
3728
- },
3729
- events: task.trigger.events,
3730
- id: task.id,
3731
- instruction,
3732
- kind: "event",
3733
- ...executionSummaryFields(stats),
3734
- ownedByViewer,
3735
- resource: `${task.trigger.label} \xB7 ${task.trigger.identifier}`,
3736
- source: task.trigger.namespace,
3737
- title: taskDisplayTitle(task.title, instruction, "Untitled event task"),
3738
- triggerAvailable: eventTaskTriggerAvailable(
3739
- task,
3740
- getResourceEventCatalog()
3835
+ async function listPersonalTokens(email, now = /* @__PURE__ */ new Date()) {
3836
+ const rows = await getDb().select().from(juniorApiTokens).where(
3837
+ and14(
3838
+ eq15(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(email)),
3839
+ isNull7(juniorApiTokens.revokedAt),
3840
+ gt(juniorApiTokens.expiresAt, now)
3741
3841
  )
3742
- };
3842
+ ).orderBy(desc5(juniorApiTokens.createdAt));
3843
+ return rows.map(metadata);
3743
3844
  }
3744
- function viewerTeamIds(user) {
3745
- return [
3746
- ...new Set(
3747
- user.identities.filter((identity) => identity.provider === "slack").map((identity) => identity.providerTenantId).filter((teamId) => Boolean(teamId))
3845
+ async function revokePersonalToken(args) {
3846
+ const rows = await getDb().update(juniorApiTokens).set({ revokedAt: args.now ?? /* @__PURE__ */ new Date() }).where(
3847
+ and14(
3848
+ eq15(juniorApiTokens.id, args.id),
3849
+ eq15(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(args.email)),
3850
+ isNull7(juniorApiTokens.revokedAt)
3748
3851
  )
3749
- ];
3750
- }
3751
- async function resolveViewerTaskCandidate(user, kind, id) {
3752
- const db = getDb();
3753
- const identityIds = new Set(user.identities.map((identity) => identity.id));
3754
- const teamIds = viewerTeamIds(user);
3755
- if (kind === "scheduled") {
3756
- const task2 = await createSchedulerSqlStore(db).getTask(id);
3757
- if (!task2 || task2.status === "deleted") return void 0;
3758
- const ownedByViewer2 = identityIds.has(task2.creatorIdentityId);
3759
- const destinations2 = await destinationDetails([task2.destination]);
3760
- const destination2 = destinations2.get(destinationKey(task2.destination));
3761
- const publicToViewer2 = teamIds.includes(task2.destination.teamId) && destination2?.visibility === "public";
3762
- if (!ownedByViewer2 && !publicToViewer2) return void 0;
3763
- return {
3764
- kind: "scheduled",
3765
- ownedByViewer: ownedByViewer2,
3766
- publicToViewer: publicToViewer2,
3767
- task: task2
3768
- };
3769
- }
3770
- const task = await getEventTask(db, id);
3771
- if (!task) return void 0;
3772
- const ownedByViewer = eventTaskBelongsToUser(task, user);
3773
- const destinations = await destinationDetails([task.destination]);
3774
- const destination = destinations.get(destinationKey(task.destination));
3775
- const publicToViewer = teamIds.includes(task.destination.teamId) && destination?.visibility === "public";
3776
- if (!ownedByViewer && !publicToViewer) return void 0;
3777
- return {
3778
- kind: "event",
3779
- ownedByViewer,
3780
- publicToViewer,
3781
- task
3782
- };
3852
+ ).returning({ id: juniorApiTokens.id });
3853
+ return rows.length > 0;
3783
3854
  }
3784
- async function taskSummaryForCandidate(candidate) {
3785
- const [destinations, creatorEmails, stats] = await Promise.all([
3786
- destinationDetails([candidate.task.destination]),
3787
- creatorProfileEmails([candidate]),
3788
- readTaskExecutionSummaries(candidate.kind, "junior")
3789
- ]);
3790
- const destination = destinations.get(
3791
- destinationKey(candidate.task.destination)
3792
- ) ?? {
3793
- label: `Channel ${candidate.task.destination.channelId}`,
3794
- visibility: "private"
3795
- };
3796
- const createdByEmail = creatorEmails.get(
3797
- creatorKey(
3798
- candidate.task.destination.teamId,
3799
- candidate.task.createdBy.slackUserId
3855
+ async function authenticatePersonalToken(token2, now = /* @__PURE__ */ new Date()) {
3856
+ if (!token2.startsWith(TOKEN_PREFIX)) return void 0;
3857
+ const rows = await getDb().select({
3858
+ id: juniorApiTokens.id,
3859
+ ownerEmailNormalized: juniorApiTokens.ownerEmailNormalized
3860
+ }).from(juniorApiTokens).where(
3861
+ and14(
3862
+ eq15(juniorApiTokens.tokenHash, hashToken(token2)),
3863
+ isNull7(juniorApiTokens.revokedAt),
3864
+ gt(juniorApiTokens.expiresAt, now)
3800
3865
  )
3801
- );
3802
- if (candidate.kind === "scheduled") {
3803
- return scheduledTaskSummary(
3804
- candidate.task,
3805
- candidate.ownedByViewer,
3806
- destination,
3807
- stats.get(candidate.task.id),
3808
- createdByEmail
3809
- );
3810
- }
3811
- return eventTaskSummary(
3812
- candidate.task,
3813
- candidate.ownedByViewer,
3814
- destination,
3815
- stats.get(candidate.task.id),
3816
- createdByEmail
3817
- );
3866
+ ).limit(1);
3867
+ const match = rows[0];
3868
+ if (!match) return void 0;
3869
+ await getDb().update(juniorApiTokens).set({ lastUsedAt: now }).where(eq15(juniorApiTokens.id, match.id));
3870
+ return match.ownerEmailNormalized;
3818
3871
  }
3819
- async function readViewerTasks(user) {
3820
- const db = getDb();
3821
- const schedulerStore = createSchedulerSqlStore(db);
3822
- const identityIds = new Set(user.identities.map((identity) => identity.id));
3823
- const teamIds = viewerTeamIds(user);
3824
- const [scheduledPage, publicScheduled, eventTasks, publicEventTasks] = await Promise.all([
3825
- createViewerScheduledTasks(schedulerStore, user).list({
3826
- limit: TASK_FETCH_LIMIT
3827
- }),
3828
- listPublicScheduledTasksForTeams(db, teamIds, TASK_FETCH_LIMIT),
3829
- listEventTasksCreatedBy(db, user, TASK_FETCH_LIMIT),
3830
- listPublicEventTasksForTeams(db, teamIds, TASK_FETCH_LIMIT)
3831
- ]);
3832
- const candidatesById = /* @__PURE__ */ new Map();
3833
- const publicScheduledIds = new Set(publicScheduled.map((task) => task.id));
3834
- const publicEventTaskIds = new Set(publicEventTasks.map((task) => task.id));
3835
- for (const task of [...scheduledPage.tasks, ...publicScheduled]) {
3836
- candidatesById.set(`scheduled:${task.id}`, {
3837
- kind: "scheduled",
3838
- ownedByViewer: identityIds.has(task.creatorIdentityId),
3839
- publicToViewer: publicScheduledIds.has(task.id),
3840
- task
3841
- });
3872
+
3873
+ // src/api/personal-tokens/create.ts
3874
+ var create_default = defineApiRoute({
3875
+ method: "post",
3876
+ path: "/",
3877
+ responseSchema: createdPersonalTokenSchema,
3878
+ handler: async (c) => {
3879
+ const email = c.get("verifiedViewerEmail");
3880
+ if (!email) throwApiError(403, "Verified viewer email required.");
3881
+ let input;
3882
+ try {
3883
+ input = await c.req.json();
3884
+ } catch (error) {
3885
+ throwApiError(400, "Invalid request body.", error);
3886
+ }
3887
+ const body = parseBody(createPersonalTokenBodySchema, input);
3888
+ return createPersonalToken({ email, name: body.name });
3842
3889
  }
3843
- for (const task of [...eventTasks, ...publicEventTasks]) {
3844
- candidatesById.set(`event:${task.id}`, {
3845
- kind: "event",
3846
- ownedByViewer: eventTaskBelongsToUser(task, user),
3847
- publicToViewer: publicEventTaskIds.has(task.id),
3848
- task
3849
- });
3890
+ });
3891
+
3892
+ // src/api/personal-tokens/list.ts
3893
+ var list_default4 = defineApiRoute({
3894
+ method: "get",
3895
+ path: "/",
3896
+ responseSchema: personalTokenListSchema,
3897
+ handler: async (c) => {
3898
+ const email = c.get("verifiedViewerEmail");
3899
+ if (!email) throwApiError(403, "Verified viewer email required.");
3900
+ return { tokens: await listPersonalTokens(email) };
3850
3901
  }
3851
- const candidates = [...candidatesById.values()].sort(
3852
- (left, right) => right.task.createdAtMs - left.task.createdAtMs || right.task.id.localeCompare(left.task.id)
3853
- );
3854
- const ownedCandidates = candidates.filter(
3855
- (candidate) => candidate.ownedByViewer
3902
+ });
3903
+
3904
+ // src/api/personal-tokens/revoke.ts
3905
+ var revoke_default = defineApiRoute({
3906
+ method: "delete",
3907
+ path: "/:id",
3908
+ responseSchema: revokePersonalTokenResponseSchema,
3909
+ handler: async (c) => {
3910
+ const email = c.get("verifiedViewerEmail");
3911
+ if (!email) throwApiError(403, "Verified viewer email required.");
3912
+ const { id } = parseParams(personalTokenParamsSchema, c.req.param());
3913
+ if (!await revokePersonalToken({ email, id })) {
3914
+ throwApiError(404, "Personal API token not found.");
3915
+ }
3916
+ return { revoked: true };
3917
+ }
3918
+ });
3919
+
3920
+ // src/api/personal-tokens/routes.ts
3921
+ var routes3 = [list_default4, create_default, revoke_default];
3922
+ function createPersonalTokenRoutes() {
3923
+ const app = new Hono4();
3924
+ registerApiRoutes(app, routes3);
3925
+ return app;
3926
+ }
3927
+
3928
+ // src/api/user-pages/routes.ts
3929
+ import { Hono as Hono5 } from "hono";
3930
+ import {
3931
+ pluginUserPageContentSchema as pluginUserPageContentSchema2,
3932
+ pluginUserPageInputSchema,
3933
+ pluginUserPageLinksSchema as pluginUserPageLinksSchema2
3934
+ } from "@sentry/junior-plugin-api";
3935
+
3936
+ // src/chat/plugins/user-pages.ts
3937
+ import {
3938
+ pluginUserPageContentSchema,
3939
+ pluginUserPageLinksSchema
3940
+ } from "@sentry/junior-plugin-api";
3941
+ function readPluginUserPageLinks() {
3942
+ return pluginUserPageLinksSchema.parse(
3943
+ getPlugins().flatMap(
3944
+ (plugin) => (plugin.userPages ?? []).map((page) => ({
3945
+ description: page.description,
3946
+ id: page.id,
3947
+ label: page.label,
3948
+ navigation: page.navigation ?? "profile",
3949
+ pluginDisplayName: plugin.manifest.displayName,
3950
+ pluginName: plugin.manifest.name
3951
+ }))
3952
+ )
3856
3953
  );
3857
- const publicCandidates = candidates.filter(
3858
- (candidate) => candidate.publicToViewer
3954
+ }
3955
+ function actionBelongsToPlugin(href, pluginName) {
3956
+ const parsed = new URL(href, "http://junior.local");
3957
+ return parsed.origin === "http://junior.local" && parsed.pathname === href && parsed.pathname.startsWith(`/api/plugins/${pluginName}/`) && !parsed.search && !parsed.hash;
3958
+ }
3959
+ async function readPluginUserPage(input) {
3960
+ const plugin = getPlugins().find(
3961
+ (candidate) => candidate.manifest.name === input.pluginName
3859
3962
  );
3860
- const selectedCandidates = /* @__PURE__ */ new Set([
3861
- ...ownedCandidates.slice(0, TASK_LIST_LIMIT),
3862
- ...publicCandidates.slice(0, TASK_LIST_LIMIT)
3863
- ]);
3864
- const selected = candidates.filter(
3865
- (candidate) => selectedCandidates.has(candidate)
3963
+ const page = plugin?.userPages?.find(
3964
+ (candidate) => candidate.id === input.pageId
3866
3965
  );
3867
- const [destinations, creatorEmails] = await Promise.all([
3868
- destinationDetails(selected.map(({ task }) => task.destination)),
3869
- creatorProfileEmails(selected)
3870
- ]);
3871
- const visible = selected.filter(
3872
- (candidate) => candidate.ownedByViewer || destinations.get(destinationKey(candidate.task.destination))?.visibility === "public"
3966
+ if (!plugin || !page) return void 0;
3967
+ const viewer2 = await resolveViewerUser(input.email);
3968
+ if (!viewer2) {
3969
+ throw new Error("Authenticated viewer user could not be resolved");
3970
+ }
3971
+ const content = pluginUserPageContentSchema.parse(
3972
+ await page.read(
3973
+ {
3974
+ db: getDb(),
3975
+ log: createPluginLogger(plugin.manifest.name),
3976
+ plugin: { name: plugin.manifest.name },
3977
+ viewer: viewer2
3978
+ },
3979
+ input.query
3980
+ )
3873
3981
  );
3874
- const [executionDays, scheduledStats, eventStats] = await Promise.all([
3875
- readTaskExecutionDays(),
3876
- readTaskExecutionSummaries("scheduled", "junior"),
3877
- readTaskExecutionSummaries("event", "junior")
3878
- ]);
3879
- const tasks = visible.map((candidate) => {
3880
- const destination = destinations.get(
3881
- destinationKey(candidate.task.destination)
3882
- ) ?? {
3883
- label: `Channel ${candidate.task.destination.channelId}`,
3884
- visibility: "private"
3885
- };
3886
- const createdByEmail = creatorEmails.get(
3887
- creatorKey(
3888
- candidate.task.destination.teamId,
3889
- candidate.task.createdBy.slackUserId
3890
- )
3982
+ if (content.records.some(
3983
+ (record) => record.actions?.some(
3984
+ (action) => !actionBelongsToPlugin(action.href, plugin.manifest.name)
3985
+ )
3986
+ )) {
3987
+ throw new Error(
3988
+ `Plugin user page "${plugin.manifest.name}/${page.id}" returned an action outside its API namespace.`
3891
3989
  );
3892
- if (candidate.kind === "scheduled") {
3893
- return scheduledTaskSummary(
3894
- candidate.task,
3895
- candidate.ownedByViewer,
3896
- destination,
3897
- scheduledStats.get(candidate.task.id),
3898
- createdByEmail
3990
+ }
3991
+ return content;
3992
+ }
3993
+
3994
+ // src/api/user-pages/routes.ts
3995
+ function createUserPageRoutes() {
3996
+ const app = new Hono5();
3997
+ app.get(
3998
+ "/",
3999
+ () => jsonResponse(pluginUserPageLinksSchema2, readPluginUserPageLinks())
4000
+ );
4001
+ app.get("/:pluginName/:pageId", async (context) => {
4002
+ const email = context.get("verifiedViewerEmail")?.trim();
4003
+ if (!email) {
4004
+ return jsonResponse(
4005
+ apiErrorSchema,
4006
+ { error: "Authentication required." },
4007
+ { status: 401 }
3899
4008
  );
3900
4009
  }
3901
- return eventTaskSummary(
3902
- candidate.task,
3903
- candidate.ownedByViewer,
3904
- destination,
3905
- eventStats.get(candidate.task.id),
3906
- createdByEmail
4010
+ const pageInput = pluginUserPageInputSchema.safeParse({
4011
+ cursor: context.req.query("cursor") || void 0,
4012
+ filter: context.req.query("filter") || void 0,
4013
+ limit: context.req.query("limit") ? Number(context.req.query("limit")) : 20,
4014
+ query: context.req.query("q") || void 0
4015
+ });
4016
+ if (!pageInput.success) {
4017
+ return jsonResponse(
4018
+ apiErrorSchema,
4019
+ { error: "Invalid user page query." },
4020
+ { status: 400 }
4021
+ );
4022
+ }
4023
+ const page = await readPluginUserPage({
4024
+ email,
4025
+ pageId: context.req.param("pageId"),
4026
+ pluginName: context.req.param("pluginName"),
4027
+ query: pageInput.data
4028
+ });
4029
+ return page ? jsonResponse(pluginUserPageContentSchema2, page) : jsonResponse(
4030
+ apiErrorSchema,
4031
+ { error: "User page not found." },
4032
+ { status: 404 }
3907
4033
  );
3908
4034
  });
3909
- return {
3910
- executionDays,
3911
- tasks,
3912
- truncated: ownedCandidates.length > TASK_LIST_LIMIT || publicCandidates.length > TASK_LIST_LIMIT
3913
- };
3914
- }
3915
- var ViewerTaskNotFoundError = class extends Error {
3916
- constructor() {
3917
- super("Task was not found.");
3918
- this.name = "ViewerTaskNotFoundError";
3919
- }
3920
- };
3921
- async function readViewerTaskExecutions(user, kind, id) {
3922
- const candidate = await resolveViewerTaskCandidate(user, kind, id);
3923
- if (!candidate) throw new ViewerTaskNotFoundError();
3924
- const [task, executions, executionDays] = await Promise.all([
3925
- taskSummaryForCandidate(candidate),
3926
- readTaskExecutions({
3927
- kind,
3928
- limit: TASK_EXECUTION_LIST_LIMIT + 1,
3929
- taskId: id
3930
- }),
3931
- readTaskExecutionStatusDays({
3932
- kind,
3933
- taskId: id
3934
- })
3935
- ]);
3936
- return {
3937
- executionDays,
3938
- executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
3939
- task,
3940
- truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
3941
- };
3942
- }
3943
- async function deleteViewerTask(user, kind, id) {
3944
- if (kind === "scheduled") {
3945
- try {
3946
- await createViewerScheduledTasks(
3947
- createSchedulerSqlStore(getDb()),
3948
- user
3949
- ).delete(id);
3950
- return;
3951
- } catch (error) {
3952
- if (error instanceof PersonalScheduledTaskNotFoundError) {
3953
- throw new ViewerTaskNotFoundError();
3954
- }
3955
- throw error;
3956
- }
3957
- }
3958
- const task = await getEventTask(getDb(), id);
3959
- if (!task || !eventTaskBelongsToUser(task, user)) {
3960
- throw new ViewerTaskNotFoundError();
3961
- }
3962
- await deleteEventTask(getDb(), id);
4035
+ return app;
3963
4036
  }
3964
4037
 
3965
4038
  // src/api/tasks/routes.ts
4039
+ import { Hono as Hono6 } from "hono";
3966
4040
  async function viewer(context) {
3967
4041
  const email = context.get("verifiedViewerEmail")?.trim();
3968
4042
  return email ? await resolveViewerUser(email) : void 0;
@@ -3977,6 +4051,14 @@ function createTaskRoutes() {
3977
4051
  { status: 401 }
3978
4052
  );
3979
4053
  });
4054
+ app.get("/runs", async (context) => {
4055
+ const user = await viewer(context);
4056
+ return user ? jsonResponse(taskRunListSchema, await readViewerTaskRuns(user)) : jsonResponse(
4057
+ apiErrorSchema,
4058
+ { error: "Authentication required." },
4059
+ { status: 401 }
4060
+ );
4061
+ });
3980
4062
  app.get("/:kind/:id/executions", async (context) => {
3981
4063
  const user = await viewer(context);
3982
4064
  if (!user) {
@@ -3997,11 +4079,7 @@ function createTaskRoutes() {
3997
4079
  try {
3998
4080
  return jsonResponse(
3999
4081
  taskExecutionListSchema,
4000
- await readViewerTaskExecutions(
4001
- user,
4002
- params.data.kind,
4003
- params.data.id
4004
- )
4082
+ await readViewerTaskExecutions(user, params.data.kind, params.data.id)
4005
4083
  );
4006
4084
  } catch (error) {
4007
4085
  if (error instanceof ViewerTaskNotFoundError) {