@sentry/junior 0.142.0 → 0.144.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 (69) hide show
  1. package/dist/{agent-hooks-WDJQQAPH.js → agent-hooks-GCUTGKY3.js} +10 -10
  2. package/dist/api/conversations/detail.d.ts +8 -0
  3. package/dist/api/schema/conversation.d.ts +18 -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 +2237 -2177
  8. package/dist/app.js +32 -32
  9. package/dist/{catalog-runtime-CLHTYBHB.js → catalog-runtime-DWVYKDC6.js} +5 -5
  10. package/dist/chat/scheduled-tasks/store.d.ts +1 -0
  11. package/dist/chat/scheduled-tasks/tool-support.d.ts +26 -1
  12. package/dist/chat/scheduled-tasks/tools/create.d.ts +10 -0
  13. package/dist/chat/scheduled-tasks/tools/delete.d.ts +10 -0
  14. package/dist/chat/scheduled-tasks/tools/list.d.ts +15 -2
  15. package/dist/chat/scheduled-tasks/tools/run-now.d.ts +10 -0
  16. package/dist/chat/scheduled-tasks/tools/update.d.ts +12 -1
  17. package/dist/chat/services/guardian-action-policy.d.ts +1 -1
  18. package/dist/chat/tasks/execution-stats.d.ts +21 -0
  19. package/dist/chat/tasks/read.d.ts +9 -1
  20. package/dist/{chunk-A646AKNB.js → chunk-3GU4PWA3.js} +2 -2
  21. package/dist/{chunk-PWDCFIQG.js → chunk-44RJWAND.js} +6 -6
  22. package/dist/{chunk-OQAPYAXH.js → chunk-4X5KM7Z6.js} +3 -3
  23. package/dist/{chunk-XFHSZNOX.js → chunk-54JMYQYS.js} +2 -2
  24. package/dist/{chunk-QDRMQI5W.js → chunk-7KAW7UF4.js} +4 -4
  25. package/dist/{chunk-SF6YKONJ.js → chunk-A67GZN73.js} +1 -1
  26. package/dist/{chunk-5QVVW3FY.js → chunk-BBCWYXHB.js} +7 -7
  27. package/dist/{chunk-B2H7NI5C.js → chunk-BEGGFVFR.js} +4 -4
  28. package/dist/{chunk-DRTWANQF.js → chunk-CNTWB2SM.js} +1 -1
  29. package/dist/{chunk-T4NFQ5SG.js → chunk-DVO5VKYQ.js} +5 -5
  30. package/dist/{chunk-OGQGJAD7.js → chunk-E7HCWOHG.js} +25 -1
  31. package/dist/{chunk-N3H7G5K6.js → chunk-GKA74DRA.js} +1 -1
  32. package/dist/{chunk-CA4WLRA3.js → chunk-HD6BHR25.js} +4 -4
  33. package/dist/{chunk-4L7PKEHM.js → chunk-HNO2E5WB.js} +1 -1
  34. package/dist/{chunk-JIWZ7T4C.js → chunk-I44JB7HQ.js} +1 -1
  35. package/dist/{chunk-R7E476CH.js → chunk-KQBXT5B7.js} +6 -6
  36. package/dist/{chunk-HVTWCMDS.js → chunk-LDWNYCZ6.js} +1 -1
  37. package/dist/{chunk-VJMBLWYR.js → chunk-PELZ2ZSY.js} +6 -6
  38. package/dist/{chunk-5ITSZIAP.js → chunk-PKH6762I.js} +1 -1
  39. package/dist/{chunk-RYYBUI34.js → chunk-QE3TICD4.js} +2 -2
  40. package/dist/{chunk-ZPNUI4L3.js → chunk-QUZJDAQQ.js} +15 -15
  41. package/dist/{chunk-XAW6IN6M.js → chunk-RIFJUJSI.js} +1 -1
  42. package/dist/{chunk-SCHH6264.js → chunk-SBZP2J5Q.js} +3 -3
  43. package/dist/{chunk-LXLBYA6N.js → chunk-SHEEZSLY.js} +2 -2
  44. package/dist/{chunk-4EZV6URQ.js → chunk-SSDTQDYQ.js} +284 -180
  45. package/dist/{chunk-LGY7YX5O.js → chunk-STFCL3K4.js} +1 -1
  46. package/dist/{chunk-CBMIVCPM.js → chunk-SUK7NC5A.js} +2 -2
  47. package/dist/{chunk-NWO5IB7V.js → chunk-UDQMY6RS.js} +1 -1
  48. package/dist/{chunk-WJEW7UNC.js → chunk-W7JFV5SB.js} +10 -10
  49. package/dist/{chunk-7R3CGEMF.js → chunk-WSOLZJJS.js} +82 -4
  50. package/dist/{chunk-LL3BGP7J.js → chunk-WTX2RFJG.js} +1 -1
  51. package/dist/{chunk-WYLSMTWU.js → chunk-ZF3N3JD6.js} +1 -1
  52. package/dist/{chunk-6IMKBHCG.js → chunk-ZZCTGAIT.js} +4 -4
  53. package/dist/cli/chat.js +32 -32
  54. package/dist/cli/check.js +6 -6
  55. package/dist/cli/plugins.js +13 -13
  56. package/dist/cli/snapshot-create.js +9 -9
  57. package/dist/cli/upgrade.js +7 -7
  58. package/dist/{db-6N4CD5AI.js → db-EFLHTBU6.js} +7 -7
  59. package/dist/instrumentation.js +4 -4
  60. package/dist/nitro.js +2 -2
  61. package/dist/{oauth-callback-server-G4K2X56R.js → oauth-callback-server-RO3IQKWM.js} +23 -23
  62. package/dist/{runner-DEIHVRUE.js → runner-LOWW7JKA.js} +15 -15
  63. package/dist/{skills-FZVJ6ZFW.js → skills-ZLMUG7AE.js} +6 -6
  64. package/dist/{spawn-WD4F67SZ.js → spawn-R3LUV6RF.js} +17 -17
  65. package/dist/{validation-EOLSG7B6.js → validation-MAKEZNTS.js} +6 -6
  66. package/dist/version.js +1 -1
  67. package/dist/{work-AS3IBMLA.js → work-5OAGC3I4.js} +16 -16
  68. package/dist/{worker-VGGNGOUU.js → worker-KTGGRTTP.js} +10 -10
  69. package/package.json +5 -5
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-E7HCWOHG.js";
32
33
  import {
33
34
  formatSlackConversationRedactedLabel,
34
35
  readHealthReport,
35
36
  resolveSlackConversationContextFromThreadId
36
- } from "./chunk-LXLBYA6N.js";
37
+ } from "./chunk-SHEEZSLY.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-7R3CGEMF.js";
46
+ readTaskExecutions,
47
+ readTaskRuns
48
+ } from "./chunk-WSOLZJJS.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-6IMKBHCG.js";
67
+ } from "./chunk-ZZCTGAIT.js";
65
68
  import {
66
69
  parseSlackThreadId
67
- } from "./chunk-5ITSZIAP.js";
70
+ } from "./chunk-PKH6762I.js";
68
71
  import {
69
72
  fallbackShortTitle
70
- } from "./chunk-XFHSZNOX.js";
73
+ } from "./chunk-54JMYQYS.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-PWDCFIQG.js";
86
+ } from "./chunk-44RJWAND.js";
84
87
  import {
85
88
  getSlackClient,
86
89
  withSlackRetries
87
- } from "./chunk-RYYBUI34.js";
90
+ } from "./chunk-QE3TICD4.js";
88
91
  import {
89
92
  decodeStoredConversationEvent,
90
93
  getDb,
91
94
  getSqlExecutor,
92
95
  locationFromRow,
93
96
  parseSessionSource
94
- } from "./chunk-SCHH6264.js";
97
+ } from "./chunk-SBZP2J5Q.js";
95
98
  import {
96
99
  juniorApiTokens,
97
100
  juniorConversationEvents,
98
101
  juniorConversations,
99
102
  juniorIdentities,
100
103
  juniorUsers
101
- } from "./chunk-4L7PKEHM.js";
104
+ } from "./chunk-HNO2E5WB.js";
102
105
  import "./chunk-EM225ITJ.js";
103
- import "./chunk-NWO5IB7V.js";
104
- import "./chunk-A646AKNB.js";
106
+ import "./chunk-UDQMY6RS.js";
107
+ import "./chunk-3GU4PWA3.js";
105
108
  import {
106
109
  juniorDestinations
107
110
  } from "./chunk-VFUK3X5B.js";
108
111
  import {
109
112
  canExposeConversationPayload
110
- } from "./chunk-LGY7YX5O.js";
113
+ } from "./chunk-STFCL3K4.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-XAW6IN6M.js";
120
+ import "./chunk-RIFJUJSI.js";
118
121
  import "./chunk-6GWA276C.js";
119
- import "./chunk-LL3BGP7J.js";
120
- import "./chunk-JIWZ7T4C.js";
122
+ import "./chunk-WTX2RFJG.js";
123
+ import "./chunk-I44JB7HQ.js";
121
124
  import "./chunk-MLKGABMK.js";
122
125
 
123
126
  // src/api.ts
@@ -1613,2356 +1616,2409 @@ 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-FZVJ6ZFW.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-CLHTYBHB.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-CLHTYBHB.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()
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
+ );
2216
2025
  });
2026
+ return {
2027
+ executionDays,
2028
+ tasks,
2029
+ truncated: ownedCandidates.length > TASK_LIST_LIMIT || publicCandidates.length > TASK_LIST_LIMIT
2030
+ };
2217
2031
  }
2218
- async function readPluginOperationalReportFeed() {
2219
- const nowMs = Date.now();
2220
- const { getPluginOperationalReports } = await import("./agent-hooks-WDJQQAPH.js");
2221
- return pluginOperationalReportFeedSchema.parse({
2222
- source: "plugins",
2223
- generatedAt: new Date(nowMs).toISOString(),
2224
- reports: await getPluginOperationalReports(nowMs)
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
+ }))
2225
2043
  });
2044
+ return {
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
2050
+ };
2226
2051
  }
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";
2052
+ var ViewerTaskNotFoundError = class extends Error {
2053
+ constructor() {
2054
+ super("Task was not found.");
2055
+ this.name = "ViewerTaskNotFoundError";
2262
2056
  }
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 } : {}
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
+ ]);
2073
+ return {
2074
+ executionDays,
2075
+ executions: executions.slice(0, TASK_EXECUTION_LIST_LIMIT),
2076
+ task,
2077
+ truncated: executions.length > TASK_EXECUTION_LIST_LIMIT
2271
2078
  };
2272
- return Object.keys(actor).length ? actor : void 0;
2273
2079
  }
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
2080
+ async function readConversationSourceTask(args) {
2081
+ const execution = await readTaskExecutionByConversationId({
2082
+ conversationId: args.conversationId
2296
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
+ return {
2097
+ id: candidate.task.id,
2098
+ kind: "scheduled",
2099
+ label: displayText(candidate.task.task.text, "Untitled scheduled task")
2100
+ };
2101
+ }
2102
+ return {
2103
+ id: candidate.task.id,
2104
+ kind: "event",
2105
+ label: displayText(candidate.task.task.text, "Untitled event task")
2106
+ };
2297
2107
  }
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
- return {
2312
- active: 0,
2313
- conversations: 0,
2314
- durationMs: 0,
2315
- failed: 0
2316
- };
2317
- }
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;
2108
+ async function deleteViewerTask(user, kind, id) {
2109
+ if (kind === "scheduled") {
2110
+ try {
2111
+ await createViewerScheduledTasks(
2112
+ createSchedulerSqlStore(getDb()),
2113
+ user
2114
+ ).delete(id);
2115
+ return;
2116
+ } catch (error) {
2117
+ if (error instanceof PersonalScheduledTaskNotFoundError) {
2118
+ throw new ViewerTaskNotFoundError();
2119
+ }
2120
+ throw error;
2121
+ }
2325
2122
  }
2326
- if (row.costUsd !== null) {
2327
- target.costUsd = Math.round(((target.costUsd ?? 0) + row.costUsd) * 1e12) / 1e12;
2123
+ const task = await getEventTask(getDb(), id);
2124
+ if (!task || !eventTaskBelongsToUser(task, user)) {
2125
+ throw new ViewerTaskNotFoundError();
2328
2126
  }
2127
+ await deleteEventTask(getDb(), id);
2329
2128
  }
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) {
2338
- return {
2339
- ...emptyMetrics(),
2340
- actor,
2341
- label: actorLabel2(actor)
2342
- };
2343
- }
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")
2352
- );
2353
- }
2354
- function locationColumns() {
2129
+
2130
+ // src/api/conversations/detail.ts
2131
+ function projectConversationDetail(args) {
2132
+ const { conversation } = args;
2133
+ const conversationId = conversation.conversationId;
2134
+ const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
2135
+ const canExposePayload = args.access?.canViewPrivateContent ?? false;
2136
+ const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
2137
+ const sentryConversationUrl = buildSentryConversationUrl(conversationId);
2355
2138
  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
2362
- };
2363
- }
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;
2377
- }
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
2139
+ ...conversationSummaryFromStoredConversation({
2140
+ access: args.access,
2141
+ auxiliaryCosts: args.auxiliaryCosts,
2142
+ conversation,
2143
+ durationMs: args.durationMs,
2144
+ ...args.locationId ? { locationId: args.locationId } : {},
2145
+ ...args.teamDomainByTeamId ? { teamDomainByTeamId: args.teamDomainByTeamId } : {},
2146
+ usage: args.usage
2386
2147
  }),
2387
- lastSeenAt: row.lastSeenAt.toISOString(),
2388
- provider: row.destinationProvider,
2389
- providerDestinationId: row.destinationProviderId,
2390
- visibility: "public"
2148
+ annotations: canExposePayload ? args.annotations : [],
2149
+ events: args.events,
2150
+ ...args.previousSeq !== void 0 ? {
2151
+ previousCursor: encodeConversationCursor({
2152
+ conversationId,
2153
+ seq: args.previousSeq
2154
+ })
2155
+ } : {},
2156
+ ...modelUsage.length > 0 ? { modelUsage } : {},
2157
+ eventHistory: conversationEventHistory({
2158
+ canExposePayload,
2159
+ ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
2160
+ }),
2161
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2162
+ ...sentryConversationUrl ? { sentryConversationUrl } : {},
2163
+ ...args.sourceTask ? { sourceTask: args.sourceTask } : {}
2391
2164
  };
2392
- addMetrics(location, row);
2393
- return location;
2394
2165
  }
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
2166
+ async function readConversationDetailFromSql(conversationId, options) {
2167
+ const record = await readConversationRecordFromSql(conversationId);
2168
+ if (!record) return void 0;
2169
+ const executor = getSqlExecutor();
2170
+ const includeDescendantMetrics = record.rootConversationId === conversationId;
2171
+ const [
2172
+ accessByConversation,
2173
+ annotations,
2174
+ auxiliaryCostsByConversation,
2175
+ modelUsage,
2176
+ metricsByRoot,
2177
+ sourceTask,
2178
+ teamDomainByTeamId
2179
+ ] = await Promise.all([
2180
+ readConversationAccessFromSql(
2181
+ getDb(),
2182
+ [conversationId],
2183
+ options.verifiedViewerEmail
2184
+ ),
2185
+ listConversationAnnotations(getDb(), conversationId),
2186
+ readConversationAuxiliaryCostsFromSql(getDb(), [conversationId], {
2187
+ includeDescendants: includeDescendantMetrics
2188
+ }),
2189
+ record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, {
2190
+ conversationId,
2191
+ includeDescendants: includeDescendantMetrics
2192
+ }) : Promise.resolve([]),
2193
+ readRootConversationMetricsFromSql(
2194
+ getDb(),
2195
+ includeDescendantMetrics ? [conversationId] : []
2196
+ ),
2197
+ readConversationSourceTask({
2198
+ conversationId,
2199
+ ...options.verifiedViewerEmail ? { verifiedViewerEmail: options.verifiedViewerEmail } : {}
2200
+ }),
2201
+ resolveSlackTeamDomains(
2202
+ record.conversation.sessionSource?.platform === "slack" ? [record.conversation.sessionSource.teamId] : []
2405
2203
  )
2406
- ).leftJoin(
2407
- juniorDestinations,
2408
- eq9(juniorDestinations.id, juniorConversations.destinationId)
2409
- ).where(topLevelWhere()).groupBy(...locationGroupBy());
2204
+ ]);
2205
+ const access = accessByConversation.get(conversationId);
2206
+ const page = record.conversation.transcriptPurgedAtMs === void 0 ? await readConversationEventPage(executor, {
2207
+ canExposePayload: access?.canViewPrivateContent ?? false,
2208
+ conversationId,
2209
+ limit: options.limit
2210
+ }) : { events: [] };
2211
+ const metrics = metricsByRoot.get(conversationId);
2212
+ return projectConversationDetail({
2213
+ ...record,
2214
+ access,
2215
+ annotations,
2216
+ auxiliaryCosts: auxiliaryCostsByConversation.get(conversationId),
2217
+ durationMs: metrics?.durationMs ?? record.durationMs,
2218
+ events: page.events,
2219
+ modelUsage,
2220
+ ...sourceTask ? { sourceTask } : {},
2221
+ teamDomainByTeamId,
2222
+ ...page.previousSeq === void 0 ? {} : { previousSeq: page.previousSeq },
2223
+ usage: metrics?.usage ?? record.usage ?? void 0
2224
+ });
2410
2225
  }
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);
2226
+ async function readConversationDetail(conversationId, options = {}) {
2227
+ const report = await readConversationDetailFromSql(conversationId, {
2228
+ ...options,
2229
+ limit: options.limit ?? 500
2230
+ });
2231
+ return report ? conversationDetailReportSchema.parse(report) : void 0;
2424
2232
  }
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
- }
2233
+ var detail_default = defineApiRoute({
2234
+ method: "get",
2235
+ path: "/:conversationId",
2236
+ responseSchema: conversationDetailReportSchema,
2237
+ handler: async (c) => {
2238
+ const { conversationId } = parseParams(
2239
+ conversationParamsSchema,
2240
+ c.req.param()
2453
2241
  );
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 } : {}
2613
- });
2614
- }
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
- }
2657
-
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;
2662
- }
2663
- var detail_default2 = defineApiRoute({
2664
- method: "get",
2665
- path: "/:locationId",
2666
- responseSchema: locationDetailReportSchema,
2667
- handler: async (c) => {
2668
- const { locationId } = parseParams(locationParamsSchema, c.req.param());
2242
+ const query = parseQuery(conversationDetailQuerySchema, c.req.query());
2669
2243
  const verifiedViewerEmail = c.get("verifiedViewerEmail");
2670
- const report = await readLocationDetail(
2671
- locationId,
2672
- verifiedViewerEmail ? { verifiedViewerEmail } : {}
2673
- );
2674
- if (!report) throwApiError(404, "Location not found.");
2244
+ const report = await readConversationDetail(conversationId, {
2245
+ ...query,
2246
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
2247
+ });
2248
+ if (!report) throwApiError(404, "Conversation not found.");
2675
2249
  return report;
2676
2250
  }
2677
2251
  });
2678
2252
 
2679
- // src/api/locations/list.ts
2680
- async function readLocationDirectory() {
2681
- return locationDirectoryReportSchema.parse(
2682
- await readLocationDirectoryFromSql()
2253
+ // src/api/conversations/event-list.ts
2254
+ async function readConversationEvents(conversationId, beforeValue, options = {}) {
2255
+ const record = await readConversationRecordFromSql(conversationId);
2256
+ if (!record) return void 0;
2257
+ const before = decodeConversationCursor({
2258
+ conversationId,
2259
+ cursor: beforeValue
2260
+ });
2261
+ if (!before) throwApiError(400, "Invalid conversation cursor.");
2262
+ const accessByConversation = await readConversationAccessFromSql(
2263
+ getDb(),
2264
+ [conversationId],
2265
+ options.verifiedViewerEmail
2683
2266
  );
2684
- }
2685
- var list_default2 = defineApiRoute({
2686
- method: "get",
2687
- path: "/",
2688
- responseSchema: locationDirectoryReportSchema,
2689
- handler: readLocationDirectory
2690
- });
2691
-
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;
2698
- }
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
2267
+ const access = accessByConversation.get(conversationId);
2268
+ const canExposePayload = access?.canViewPrivateContent ?? false;
2269
+ const transcriptPurgedAtMs = record.conversation.transcriptPurgedAtMs;
2270
+ const page = transcriptPurgedAtMs === void 0 ? await readConversationEventPage(getSqlExecutor(), {
2271
+ beforeSeq: before.seq,
2272
+ canExposePayload,
2273
+ conversationId,
2274
+ limit: options.limit ?? 500
2275
+ }) : { events: [] };
2276
+ return conversationEventPageSchema.parse({
2277
+ events: page.events,
2278
+ eventHistory: conversationEventHistory({
2279
+ canExposePayload,
2280
+ ...transcriptPurgedAtMs === void 0 ? {} : { transcriptPurgedAtMs }
2281
+ }),
2282
+ ...page.previousSeq === void 0 ? {} : {
2283
+ previousCursor: encodeConversationCursor({
2284
+ conversationId,
2285
+ seq: page.previousSeq
2286
+ })
2287
+ },
2288
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
2289
+ });
2290
+ }
2291
+ var event_list_default = defineApiRoute({
2292
+ method: "get",
2293
+ path: "/:conversationId/events",
2294
+ responseSchema: conversationEventPageSchema,
2295
+ handler: async (c) => {
2296
+ const { conversationId } = parseParams(
2297
+ conversationParamsSchema,
2298
+ c.req.param()
2299
+ );
2300
+ const { before, limit } = parseQuery(
2301
+ conversationEventsQuerySchema,
2302
+ c.req.query()
2303
+ );
2304
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
2305
+ const report = await readConversationEvents(conversationId, before, {
2306
+ limit,
2307
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
2308
+ });
2309
+ if (!report) throwApiError(404, "Conversation not found.");
2310
+ return report;
2311
+ }
2723
2312
  });
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
- );
2313
+
2314
+ // src/api/conversations/stats.query.ts
2315
+ import { and as and8, eq as eq9, gte, isNull as isNull4, lte, sql as sql6 } from "drizzle-orm";
2316
+ import { alias as alias4 } from "drizzle-orm/pg-core";
2317
+
2318
+ // src/api/conversations/aggregate.ts
2319
+ import { sql as sql5 } from "drizzle-orm";
2320
+ function tokenValue(source) {
2321
+ return sql5`
2322
+ CASE
2323
+ WHEN COALESCE(
2324
+ ${source.usage}->>'inputTokens',
2325
+ ${source.usage}->>'outputTokens',
2326
+ ${source.usage}->>'cachedInputTokens',
2327
+ ${source.usage}->>'cacheCreationTokens'
2328
+ ) IS NOT NULL
2329
+ THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
2330
+ + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
2331
+ + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
2332
+ + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
2333
+ WHEN ${source.usage}->>'totalTokens' IS NOT NULL
2334
+ THEN (${source.usage}->>'totalTokens')::double precision
2335
+ ELSE NULL
2336
+ END
2337
+ `;
2765
2338
  }
2766
- function normalizeEmail(email) {
2767
- const normalized = email?.trim().toLowerCase();
2768
- return normalized || void 0;
2339
+ function costValue(source) {
2340
+ return sql5`
2341
+ CASE
2342
+ WHEN ${source.usage}->'cost'->>'total' IS NOT NULL
2343
+ THEN (${source.usage}->'cost'->>'total')::double precision
2344
+ WHEN COALESCE(
2345
+ ${source.usage}->'cost'->>'input',
2346
+ ${source.usage}->'cost'->>'output',
2347
+ ${source.usage}->'cost'->>'cacheRead',
2348
+ ${source.usage}->'cost'->>'cacheWrite'
2349
+ ) IS NOT NULL
2350
+ THEN COALESCE((${source.usage}->'cost'->>'input')::double precision, 0)
2351
+ + COALESCE((${source.usage}->'cost'->>'output')::double precision, 0)
2352
+ + COALESCE((${source.usage}->'cost'->>'cacheRead')::double precision, 0)
2353
+ + COALESCE((${source.usage}->'cost'->>'cacheWrite')::double precision, 0)
2354
+ ELSE NULL
2355
+ END
2356
+ `;
2769
2357
  }
2770
- function emptyTotals() {
2358
+ function conversationAggregateColumns(sources) {
2359
+ const metrics = sources?.metrics ?? juniorConversations;
2360
+ const roots = sources?.roots ?? juniorConversations;
2361
+ const conversationCount = sources ? sql5`COUNT(DISTINCT ${roots.conversationId})` : sql5`COUNT(*)`;
2771
2362
  return {
2772
- active: 0,
2773
- activeDays: 0,
2774
- conversations: 0,
2775
- durationMs: 0,
2776
- failed: 0
2363
+ active: sql5`${conversationCount} FILTER (
2364
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
2365
+ )::integer`,
2366
+ conversations: sql5`${conversationCount}::integer`,
2367
+ costUsd: sql5`SUM(${costValue(metrics)})::double precision`,
2368
+ durationMs: sql5`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
2369
+ failed: sql5`${conversationCount} FILTER (
2370
+ WHERE ${roots.executionStatus} = 'failed'
2371
+ )::integer`,
2372
+ tokens: sql5`SUM(${tokenValue(metrics)})::double precision`
2777
2373
  };
2778
2374
  }
2779
- function emptyActivityDay(date) {
2375
+ function conversationRangeColumns() {
2376
+ return {
2377
+ firstSeenAt: sql5`MIN(${juniorConversations.createdAt})`.mapWith(
2378
+ juniorConversations.createdAt
2379
+ ),
2380
+ lastSeenAt: sql5`MAX(${juniorConversations.lastActivityAt})`.mapWith(
2381
+ juniorConversations.lastActivityAt
2382
+ )
2383
+ };
2384
+ }
2385
+ function conversationActiveDaysColumn() {
2386
+ return sql5`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
2387
+ }
2388
+
2389
+ // src/api/conversations/stats.query.ts
2390
+ var WINDOW_DAYS = 90;
2391
+ var treeConversation = alias4(juniorConversations, "stats_tree_conversation");
2392
+ var treeAggregateColumns = conversationAggregateColumns({
2393
+ metrics: treeConversation,
2394
+ roots: juniorConversations
2395
+ });
2396
+ function emptyStatsItem(label) {
2780
2397
  return {
2781
2398
  active: 0,
2782
2399
  conversations: 0,
2783
- date,
2784
2400
  durationMs: 0,
2785
- failed: 0
2401
+ failed: 0,
2402
+ label
2786
2403
  };
2787
2404
  }
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));
2405
+ function addUsd(current, next) {
2406
+ return Math.round(((current ?? 0) + next) * 1e12) / 1e12;
2407
+ }
2408
+ function actorLabel(row) {
2409
+ return row.userEmail?.trim() || row.identityEmail?.trim() || row.userDisplayName?.trim() || row.identityDisplayName?.trim() || row.identityHandle?.trim() || row.identitySubjectId?.trim() || "Unknown";
2410
+ }
2411
+ function surfaceLabel(source) {
2412
+ if (source === "scheduler") return "Scheduler";
2413
+ if (source === "api") return "API";
2414
+ if (source === "internal" || source === "local") return "Internal";
2415
+ return "Conversation";
2416
+ }
2417
+ function locationLabel(row) {
2418
+ if (row.destinationProvider !== "slack") {
2419
+ return surfaceLabel(row.source);
2797
2420
  }
2798
- return items;
2421
+ if (row.destinationKind === "dm") {
2422
+ return "Direct Message";
2423
+ }
2424
+ if (row.destinationVisibility !== "public") {
2425
+ return "Private Conversation";
2426
+ }
2427
+ const name = (row.channelName ?? row.destinationDisplayName)?.trim().replace(/^#/, "");
2428
+ return name ? `#${name}` : "Public Channel";
2799
2429
  }
2800
- function statsItems2(map) {
2430
+ function addAggregate(map, label, row) {
2431
+ const item = map.get(label) ?? emptyStatsItem(label);
2432
+ item.active += row.active;
2433
+ item.conversations += row.conversations;
2434
+ item.durationMs += row.durationMs;
2435
+ item.failed += row.failed;
2436
+ if (row.tokens !== null) {
2437
+ item.tokens = (item.tokens ?? 0) + row.tokens;
2438
+ }
2439
+ if (row.costUsd !== null) {
2440
+ item.costUsd = addUsd(item.costUsd, row.costUsd);
2441
+ }
2442
+ map.set(label, item);
2443
+ }
2444
+ function statsItems(map) {
2801
2445
  return [...map.values()].sort(
2802
- (left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
2446
+ (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
2803
2447
  );
2804
2448
  }
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
2449
+ function statsWhere(start, end) {
2450
+ return and8(
2451
+ isNull4(juniorConversations.parentConversationId),
2452
+ gte(juniorConversations.lastActivityAt, start),
2453
+ lte(juniorConversations.lastActivityAt, end)
2812
2454
  );
2813
2455
  }
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) {
2456
+ function statsWindow(nowMs) {
2849
2457
  const end = new Date(nowMs);
2458
+ const start = new Date(nowMs);
2459
+ start.setUTCHours(0, 0, 0, 0);
2460
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2461
+ return { end, start };
2462
+ }
2463
+ function metricDays(rows, endMs) {
2464
+ const byDate = new Map(rows.map((row) => [row.date, row]));
2465
+ const end = new Date(endMs);
2850
2466
  end.setUTCHours(0, 0, 0, 0);
2851
2467
  const start = new Date(end);
2852
- start.setUTCDate(start.getUTCDate() - (DIRECTORY_ACTIVITY_DAYS - 1));
2853
- return { end, start };
2468
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2469
+ const days = [];
2470
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2471
+ const date = cursor.toISOString().slice(0, 10);
2472
+ const row = byDate.get(date);
2473
+ days.push({
2474
+ conversations: row?.conversations ?? 0,
2475
+ date,
2476
+ durationMs: row?.durationMs ?? 0,
2477
+ ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: addUsd(void 0, row.costUsd) } : {},
2478
+ ...row?.tokens !== null && row?.tokens !== void 0 ? { tokens: row.tokens } : {}
2479
+ });
2480
+ }
2481
+ return days;
2854
2482
  }
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 = [];
2483
+ function guardianStats(rows, endMs) {
2484
+ const byDate = new Map(rows.map((row) => [row.date, row]));
2485
+ const end = new Date(endMs);
2486
+ end.setUTCHours(0, 0, 0, 0);
2487
+ const start = new Date(end);
2488
+ start.setUTCDate(start.getUTCDate() - (WINDOW_DAYS - 1));
2489
+ const metricDays2 = [];
2490
+ let allow = 0;
2491
+ let ask = 0;
2492
+ let deny = 0;
2493
+ let requests = 0;
2494
+ let costUsd;
2859
2495
  for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2860
2496
  const date = cursor.toISOString().slice(0, 10);
2861
- items.push(days.get(date) ?? { activePeople: 0, conversations: 0, date });
2497
+ const row = byDate.get(date);
2498
+ allow += row?.allow ?? 0;
2499
+ ask += row?.ask ?? 0;
2500
+ deny += row?.deny ?? 0;
2501
+ requests += row?.requests ?? 0;
2502
+ if (row?.costUsd !== null && row?.costUsd !== void 0) {
2503
+ costUsd = addUsd(costUsd, row.costUsd);
2504
+ }
2505
+ metricDays2.push({
2506
+ allow: row?.allow ?? 0,
2507
+ ask: row?.ask ?? 0,
2508
+ date,
2509
+ deny: row?.deny ?? 0,
2510
+ requests: row?.requests ?? 0,
2511
+ ...row?.costUsd !== null && row?.costUsd !== void 0 ? { costUsd: row.costUsd } : {}
2512
+ });
2862
2513
  }
2863
- return items;
2514
+ return {
2515
+ allow,
2516
+ ask,
2517
+ deny,
2518
+ metricDays: metricDays2,
2519
+ requests,
2520
+ ...costUsd !== void 0 ? { costUsd } : {}
2521
+ };
2864
2522
  }
2865
- async function readPeopleListFromSql() {
2866
- const nowMs = Date.now();
2867
- const { end, start } = activityWindow(nowMs);
2868
- const activityDate = sql9`TO_CHAR(
2523
+ async function aggregateStats(db, start, end) {
2524
+ const where = statsWhere(start, end);
2525
+ const activityDate = sql6`TO_CHAR(
2869
2526
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2870
2527
  'YYYY-MM-DD'
2871
2528
  )`;
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()
2529
+ const [totalsRows, actorRows, locationRows, metricRows] = await Promise.all([
2530
+ db.select(treeAggregateColumns).from(juniorConversations).innerJoin(
2531
+ treeConversation,
2532
+ eq9(
2533
+ treeConversation.rootConversationId,
2534
+ juniorConversations.conversationId
2535
+ )
2536
+ ).where(where),
2537
+ db.select({
2538
+ identityDisplayName: juniorIdentities.displayName,
2539
+ identityEmail: juniorIdentities.emailNormalized,
2540
+ identityHandle: juniorIdentities.handle,
2541
+ identitySubjectId: juniorIdentities.providerSubjectId,
2542
+ userDisplayName: juniorUsers.displayName,
2543
+ userEmail: juniorUsers.primaryEmailNormalized,
2544
+ ...treeAggregateColumns
2881
2545
  }).from(juniorConversations).innerJoin(
2546
+ treeConversation,
2547
+ eq9(
2548
+ treeConversation.rootConversationId,
2549
+ juniorConversations.conversationId
2550
+ )
2551
+ ).leftJoin(
2882
2552
  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
2553
+ eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
2554
+ ).leftJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
2555
+ juniorIdentities.displayName,
2556
+ juniorIdentities.emailNormalized,
2557
+ juniorIdentities.handle,
2558
+ juniorIdentities.providerSubjectId,
2559
+ juniorUsers.displayName,
2560
+ juniorUsers.primaryEmailNormalized
2561
+ ),
2562
+ db.select({
2563
+ channelName: juniorConversations.channelName,
2564
+ destinationDisplayName: juniorDestinations.displayName,
2565
+ destinationKind: juniorDestinations.kind,
2566
+ destinationProvider: juniorDestinations.provider,
2567
+ destinationVisibility: juniorDestinations.visibility,
2568
+ source: juniorConversations.source,
2569
+ ...treeAggregateColumns
2889
2570
  }).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)
2571
+ treeConversation,
2572
+ eq9(
2573
+ treeConversation.rootConversationId,
2574
+ juniorConversations.conversationId
2896
2575
  )
2897
- ).groupBy(activityDate)
2898
- ]);
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
- });
2918
- return {
2919
- activityDays: directoryActivityDays2(activityRows, nowMs),
2920
- 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)
2576
+ ).leftJoin(
2577
+ juniorDestinations,
2578
+ eq9(juniorDestinations.id, juniorConversations.destinationId)
2579
+ ).where(where).groupBy(
2580
+ juniorConversations.channelName,
2581
+ juniorConversations.source,
2582
+ juniorDestinations.displayName,
2583
+ juniorDestinations.kind,
2584
+ juniorDestinations.provider,
2585
+ juniorDestinations.visibility
2923
2586
  ),
2924
- source: "conversation_index",
2925
- windowEnd: end.toISOString(),
2926
- windowStart: start.toISOString()
2927
- };
2587
+ db.select({
2588
+ conversations: treeAggregateColumns.conversations,
2589
+ costUsd: treeAggregateColumns.costUsd,
2590
+ date: activityDate,
2591
+ durationMs: treeAggregateColumns.durationMs,
2592
+ tokens: treeAggregateColumns.tokens
2593
+ }).from(juniorConversations).innerJoin(
2594
+ treeConversation,
2595
+ eq9(
2596
+ treeConversation.rootConversationId,
2597
+ juniorConversations.conversationId
2598
+ )
2599
+ ).where(where).groupBy(activityDate)
2600
+ ]);
2601
+ return { actorRows, locationRows, metricRows, totals: totalsRows[0] };
2928
2602
  }
2929
-
2930
- // src/api/people/list.ts
2931
- async function readPeopleList() {
2932
- return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
2603
+ async function aggregateGuardianStats(db, start, end) {
2604
+ const date = sql6`TO_CHAR(
2605
+ ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
2606
+ 'YYYY-MM-DD'
2607
+ )`;
2608
+ const decision = sql6`${juniorConversationEvents.payload}->>'decision'`;
2609
+ const cost2 = sql6`CASE
2610
+ WHEN jsonb_typeof(${juniorConversationEvents.payload}->'costUsd') = 'number'
2611
+ THEN (${juniorConversationEvents.payload}->>'costUsd')::double precision
2612
+ ELSE NULL
2613
+ END`;
2614
+ return await db.select({
2615
+ allow: sql6`COUNT(*) FILTER (WHERE ${decision} = 'allow')::integer`,
2616
+ ask: sql6`COUNT(*) FILTER (WHERE ${decision} = 'ask')::integer`,
2617
+ costUsd: sql6`SUM(${cost2})::double precision`,
2618
+ date,
2619
+ deny: sql6`COUNT(*) FILTER (WHERE ${decision} = 'deny')::integer`,
2620
+ requests: sql6`COUNT(*)::integer`
2621
+ }).from(juniorConversationEvents).where(
2622
+ and8(
2623
+ eq9(juniorConversationEvents.type, "guardian_action_reviewed"),
2624
+ gte(juniorConversationEvents.createdAt, start),
2625
+ lte(juniorConversationEvents.createdAt, end)
2626
+ )
2627
+ ).groupBy(date);
2933
2628
  }
2934
- var list_default3 = defineApiRoute({
2935
- method: "get",
2936
- path: "/",
2937
- responseSchema: actorDirectoryReportSchema,
2938
- handler: readPeopleList
2939
- });
2940
-
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),
2950
- 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()
2959
- };
2960
- }
2961
- function addAggregate2(map, label, row) {
2962
- const item = map.get(label) ?? {
2963
- active: 0,
2964
- conversations: 0,
2965
- durationMs: 0,
2966
- failed: 0,
2967
- label
2968
- };
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
- }
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`;
2985
- }
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";
2994
- }
2995
- async function readPeopleProfileFromSql(email, options = {}) {
2629
+ async function readConversationStatsFromSql() {
2996
2630
  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)
2631
+ const { end, start } = statsWindow(nowMs);
2632
+ const db = getDb();
2633
+ const [{ actorRows, locationRows, metricRows, totals }, guardianRows] = await Promise.all([
2634
+ aggregateStats(db, start, end),
2635
+ aggregateGuardianStats(db, start, end)
3053
2636
  ]);
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
- }
2637
+ const actors = /* @__PURE__ */ new Map();
3074
2638
  const locations = /* @__PURE__ */ new Map();
3075
- for (const row of locationRows) {
3076
- addAggregate2(locations, locationLabel2(row), row);
2639
+ for (const row of actorRows) {
2640
+ addAggregate(actors, actorLabel(row), row);
3077
2641
  }
3078
- const surfaces = /* @__PURE__ */ new Map();
3079
- for (const row of surfaceRows) {
3080
- addAggregate2(
3081
- surfaces,
3082
- surfaceLabel2(row.surface),
3083
- row
3084
- );
2642
+ for (const row of locationRows) {
2643
+ addAggregate(locations, locationLabel(row), row);
3085
2644
  }
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
- ]);
3095
2645
  return {
3096
- activityDays: activityDays2(days, nowMs),
2646
+ active: totals?.active ?? 0,
2647
+ conversations: totals?.conversations ?? 0,
2648
+ durationMs: totals?.durationMs ?? 0,
2649
+ failed: totals?.failed ?? 0,
3097
2650
  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
- ),
3105
- actor,
2651
+ guardian: guardianStats(guardianRows, nowMs),
2652
+ metricDays: metricDays(metricRows, nowMs),
2653
+ locations: statsItems(locations),
2654
+ actors: statsItems(actors),
3106
2655
  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
- },
2656
+ ...totals?.costUsd !== null && totals?.costUsd !== void 0 ? { costUsd: addUsd(void 0, totals.costUsd) } : {},
2657
+ ...totals?.tokens !== null && totals?.tokens !== void 0 ? { tokens: totals.tokens } : {},
3116
2658
  windowEnd: end.toISOString(),
3117
2659
  windowStart: start.toISOString()
3118
2660
  };
3119
2661
  }
3120
2662
 
3121
- // src/api/people/profile.ts
3122
- async function readPeopleProfile(email, options = {}) {
3123
- return actorProfileReportSchema.parse(
3124
- await readPeopleProfileFromSql(email, options)
2663
+ // src/api/conversations/stats.ts
2664
+ async function readConversationStats() {
2665
+ return conversationStatsReportSchema.parse(
2666
+ await readConversationStatsFromSql()
3125
2667
  );
3126
2668
  }
3127
- var profile_default = defineApiRoute({
2669
+ var stats_default = defineApiRoute({
3128
2670
  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
- );
3138
- }
2671
+ path: "/stats",
2672
+ responseSchema: conversationStatsReportSchema,
2673
+ handler: readConversationStats
3139
2674
  });
3140
2675
 
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;
2676
+ // src/api/conversations/routes.ts
2677
+ var routes = [
2678
+ list_default,
2679
+ stats_default,
2680
+ archive_default,
2681
+ event_list_default,
2682
+ detail_default
2683
+ ];
2684
+ function createConversationRoutes() {
2685
+ const app = new Hono();
2686
+ registerApiRoutes(app, routes);
2687
+ return app;
3147
2688
  }
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 };
2689
+
2690
+ // src/reporting.ts
2691
+ import { readFileSync } from "fs";
2692
+ import path from "path";
2693
+ function readDescriptionText(home) {
2694
+ try {
2695
+ const raw = readFileSync(path.join(home, "DESCRIPTION.md"), "utf8").trim();
2696
+ return raw || void 0;
2697
+ } catch {
2698
+ return void 0;
2699
+ }
3157
2700
  }
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(
3162
- ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3163
- 'YYYY-MM-DD'
3164
- )`;
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
- };
2701
+ async function readSkillReports() {
2702
+ const { discoverSkills } = await import("./skills-ZLMUG7AE.js");
2703
+ const skills = await discoverSkills();
2704
+ return skillReportsSchema.parse(
2705
+ skills.map((skill) => ({
2706
+ name: skill.name,
2707
+ pluginProvider: skill.pluginProvider
2708
+ }))
2709
+ );
3197
2710
  }
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
3215
- };
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;
3222
- }
3223
- }
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
- }
2711
+ async function readPlugins() {
2712
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-DWVYKDC6.js");
2713
+ return pluginsSchema.parse(
2714
+ pluginCatalogRuntime.getProviders().map((plugin) => ({
2715
+ configKeys: plugin.manifest.configKeys ?? [],
2716
+ description: plugin.manifest.description,
2717
+ displayName: plugin.manifest.displayName,
2718
+ name: plugin.manifest.name
2719
+ }))
2720
+ );
2721
+ }
2722
+ async function readRuntimeInfoReport() {
2723
+ const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
2724
+ import("./discovery-GUFTFBTL.js"),
2725
+ import("./catalog-runtime-DWVYKDC6.js"),
2726
+ readPlugins(),
2727
+ readSkillReports()
2728
+ ]);
2729
+ const home = homeDir();
2730
+ return runtimeInfoReportSchema.parse({
2731
+ cwd: process.cwd(),
2732
+ homeDir: home,
2733
+ descriptionText: readDescriptionText(home),
2734
+ providers: plugins.map((plugin) => plugin.name),
2735
+ skills,
2736
+ packagedContent: pluginCatalogRuntime.getPackageContent()
3233
2737
  });
3234
2738
  }
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;
2739
+ async function readPluginOperationalReportFeed() {
2740
+ const nowMs = Date.now();
2741
+ const { getPluginOperationalReports } = await import("./agent-hooks-GCUTGKY3.js");
2742
+ return pluginOperationalReportFeedSchema.parse({
2743
+ source: "plugins",
2744
+ generatedAt: new Date(nowMs).toISOString(),
2745
+ reports: await getPluginOperationalReports(nowMs)
2746
+ });
3246
2747
  }
3247
2748
 
3248
- // src/api/plugin-reports.ts
3249
- var plugin_reports_default = defineApiRoute({
2749
+ // src/api/health.ts
2750
+ var health_default = defineApiRoute({
3250
2751
  method: "get",
3251
- path: "/api/plugin-reports",
3252
- responseSchema: pluginOperationalReportFeedSchema,
3253
- handler: readPluginOperationalReportFeed
2752
+ path: "/api/health",
2753
+ responseSchema: healthReportSchema,
2754
+ handler: readHealthReport
3254
2755
  });
3255
2756
 
3256
- // src/api/personal-tokens/routes.ts
3257
- import { Hono as Hono4 } from "hono";
2757
+ // src/api/locations/routes.ts
2758
+ import { Hono as Hono2 } from "hono";
3258
2759
 
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();
2760
+ // src/api/locations/query.ts
2761
+ 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";
2762
+ import { alias as alias5 } from "drizzle-orm/pg-core";
2763
+
2764
+ // src/api/conversations/reporting.ts
2765
+ function surfaceLabel2(surface) {
2766
+ if (surface === "scheduler") return "Scheduler";
2767
+ if (surface === "api") return "API";
2768
+ if (surface === "internal") return "Internal";
2769
+ return "Conversation";
3267
2770
  }
3268
- function hashToken(token2) {
3269
- return createHash("sha256").update(token2, "utf8").digest("hex");
2771
+ function slackLocationLabel(args) {
2772
+ const channelId = args.channel;
2773
+ if (!channelId) return void 0;
2774
+ if (args.channelNameRedacted && args.channelName) return args.channelName;
2775
+ const name = args.channelName?.replace(/^#/, "");
2776
+ if (channelId.startsWith("D")) return "Direct Message";
2777
+ if (channelId.startsWith("C")) {
2778
+ return name ? `#${name}` : "Public Channel";
2779
+ }
2780
+ if (channelId.startsWith("G")) {
2781
+ if (name?.startsWith("mpdm-")) return "Group DM";
2782
+ return "Private Channel";
2783
+ }
2784
+ return name || channelId;
3270
2785
  }
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
2786
+ function actorFromRow(row) {
2787
+ const actor = {
2788
+ ...row.email ? { email: row.email } : {},
2789
+ ...row.fullName ? { fullName: row.fullName } : {},
2790
+ ...row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
2791
+ ...row.handle ? { slackUserName: row.handle } : {}
3279
2792
  };
2793
+ return Object.keys(actor).length ? actor : void 0;
3280
2794
  }
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)
2795
+ function summaryFromRow(row, options = {}) {
2796
+ const actor = actorFromRow(row);
2797
+ const conversation = {
2798
+ conversationId: row.conversationId,
2799
+ createdAtMs: row.createdAt.getTime(),
2800
+ lastActivityAtMs: row.lastActivityAt.getTime(),
2801
+ updatedAtMs: row.updatedAt.getTime(),
2802
+ execution: {
2803
+ status: row.executionStatus,
2804
+ ...row.executionUpdatedAt ? { updatedAtMs: row.executionUpdatedAt.getTime() } : {}
2805
+ },
2806
+ ...actor ? { actor } : {},
2807
+ ...row.channelName ? { channelName: row.channelName } : {},
2808
+ ...row.source ? { source: row.source } : {},
2809
+ ...row.title ? { title: row.title } : {}
3292
2810
  };
3293
- await getDb().insert(juniorApiTokens).values(row);
3294
- return { ...metadata({ ...row, lastUsedAt: null, revokedAt: null }), token: token2 };
2811
+ return conversationSummaryFromStoredConversation({
2812
+ access: options.access,
2813
+ conversation,
2814
+ durationMs: options.metrics?.durationMs ?? row.durationMs,
2815
+ ...options.access?.visibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
2816
+ usage: options.metrics?.usage ?? row.usage ?? void 0
2817
+ });
3295
2818
  }
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);
2819
+
2820
+ // src/api/locations/query.ts
2821
+ var RECENT_LIMIT = 25;
2822
+ var ACTIVITY_DAYS = 90;
2823
+ var treeConversation2 = alias5(
2824
+ juniorConversations,
2825
+ "location_tree_conversation"
2826
+ );
2827
+ var treeAggregateColumns2 = conversationAggregateColumns({
2828
+ metrics: treeConversation2,
2829
+ roots: juniorConversations
2830
+ });
2831
+ function emptyMetrics() {
2832
+ return {
2833
+ active: 0,
2834
+ conversations: 0,
2835
+ durationMs: 0,
2836
+ failed: 0
2837
+ };
3305
2838
  }
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;
2839
+ function addMetrics(target, row) {
2840
+ target.active += row.active;
2841
+ target.conversations += row.conversations;
2842
+ target.durationMs += row.durationMs;
2843
+ target.failed += row.failed;
2844
+ if (row.tokens !== null) {
2845
+ target.tokens = (target.tokens ?? 0) + row.tokens;
2846
+ }
2847
+ if (row.costUsd !== null) {
2848
+ target.costUsd = Math.round(((target.costUsd ?? 0) + row.costUsd) * 1e12) / 1e12;
2849
+ }
3315
2850
  }
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)
3326
- )
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;
2851
+ function publicLabel(input) {
2852
+ const name = input.displayName?.trim().replace(/^#/, "");
2853
+ return name ? `#${name}` : `Public channel ${input.providerDestinationId}`;
3332
2854
  }
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;
2855
+ function actorLabel2(actor) {
2856
+ return actor.email?.trim() || actor.fullName?.trim() || actor.slackUserName?.trim() || actor.slackUserId?.trim() || "Unknown";
3387
2857
  }
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
- );
2858
+ function emptyActor(actor) {
2859
+ return {
2860
+ ...emptyMetrics(),
2861
+ actor,
2862
+ label: actorLabel2(actor)
2863
+ };
3415
2864
  }
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;
2865
+ function topLevelWhere() {
2866
+ return isNull5(juniorConversations.parentConversationId);
3419
2867
  }
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
2868
+ function publicLocationWhere(destinationId) {
2869
+ return and9(
2870
+ topLevelWhere(),
2871
+ eq10(juniorDestinations.id, destinationId),
2872
+ eq10(juniorDestinations.visibility, "public")
3426
2873
  );
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");
2874
+ }
2875
+ function locationColumns() {
2876
+ return {
2877
+ destinationDisplayName: juniorDestinations.displayName,
2878
+ destinationId: juniorDestinations.id,
2879
+ destinationKind: juniorDestinations.kind,
2880
+ destinationProvider: juniorDestinations.provider,
2881
+ destinationProviderId: juniorDestinations.providerDestinationId,
2882
+ destinationVisibility: juniorDestinations.visibility
2883
+ };
2884
+ }
2885
+ function locationGroupBy() {
2886
+ return [
2887
+ juniorDestinations.displayName,
2888
+ juniorDestinations.id,
2889
+ juniorDestinations.kind,
2890
+ juniorDestinations.provider,
2891
+ juniorDestinations.providerDestinationId,
2892
+ juniorDestinations.visibility
2893
+ ];
2894
+ }
2895
+ function locationFromAggregate(row) {
2896
+ if (row.destinationVisibility !== "public" || !row.destinationId || !row.destinationProvider || !row.destinationProviderId || !row.destinationKind) {
2897
+ return void 0;
3431
2898
  }
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)
2899
+ const location = {
2900
+ ...emptyMetrics(),
2901
+ firstSeenAt: row.firstSeenAt.toISOString(),
2902
+ id: row.destinationId,
2903
+ kind: row.destinationKind,
2904
+ label: publicLabel({
2905
+ displayName: row.destinationDisplayName,
2906
+ providerDestinationId: row.destinationProviderId
2907
+ }),
2908
+ lastSeenAt: row.lastSeenAt.toISOString(),
2909
+ provider: row.destinationProvider,
2910
+ providerDestinationId: row.destinationProviderId,
2911
+ visibility: "public"
2912
+ };
2913
+ addMetrics(location, row);
2914
+ return location;
2915
+ }
2916
+ async function directoryRows(db) {
2917
+ return db.select({
2918
+ ...locationColumns(),
2919
+ ...treeAggregateColumns2,
2920
+ ...conversationRangeColumns()
2921
+ }).from(juniorConversations).innerJoin(
2922
+ treeConversation2,
2923
+ eq10(
2924
+ treeConversation2.rootConversationId,
2925
+ juniorConversations.conversationId
3446
2926
  )
3447
- )) {
3448
- throw new Error(
3449
- `Plugin user page "${plugin.manifest.name}/${page.id}" returned an action outside its API namespace.`
2927
+ ).leftJoin(
2928
+ juniorDestinations,
2929
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
2930
+ ).where(topLevelWhere()).groupBy(...locationGroupBy());
2931
+ }
2932
+ async function directoryActivityRows(db, start) {
2933
+ const date = sql7`TO_CHAR(
2934
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
2935
+ 'YYYY-MM-DD'
2936
+ )`;
2937
+ return db.select({
2938
+ conversations: sql7`COUNT(*)::integer`,
2939
+ date,
2940
+ visibility: juniorDestinations.visibility
2941
+ }).from(juniorConversations).leftJoin(
2942
+ juniorDestinations,
2943
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
2944
+ ).where(and9(topLevelWhere(), gte2(juniorConversations.lastActivityAt, start))).groupBy(date, juniorDestinations.visibility);
2945
+ }
2946
+ function directoryActivityDays(rows, nowMs) {
2947
+ const days = /* @__PURE__ */ new Map();
2948
+ for (const row of rows) {
2949
+ const day = days.get(row.date) ?? {
2950
+ date: row.date,
2951
+ privateConversations: 0,
2952
+ publicConversations: 0
2953
+ };
2954
+ if (row.visibility === "public") {
2955
+ day.publicConversations += row.conversations;
2956
+ } else {
2957
+ day.privateConversations += row.conversations;
2958
+ }
2959
+ days.set(row.date, day);
2960
+ }
2961
+ const end = new Date(nowMs);
2962
+ end.setUTCHours(0, 0, 0, 0);
2963
+ const start = new Date(end);
2964
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2965
+ const activity = [];
2966
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
2967
+ const date = cursor.toISOString().slice(0, 10);
2968
+ activity.push(
2969
+ days.get(date) ?? {
2970
+ date,
2971
+ privateConversations: 0,
2972
+ publicConversations: 0
2973
+ }
3450
2974
  );
3451
2975
  }
3452
- return content;
2976
+ return activity;
3453
2977
  }
3454
-
3455
- // src/api/user-pages/routes.ts
3456
- function createUserPageRoutes() {
3457
- const app = new Hono5();
3458
- app.get(
3459
- "/",
3460
- () => jsonResponse(pluginUserPageLinksSchema2, readPluginUserPageLinks())
3461
- );
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
2978
+ async function readLocationDirectoryFromSql() {
2979
+ const nowMs = Date.now();
2980
+ const end = new Date(nowMs);
2981
+ end.setUTCHours(0, 0, 0, 0);
2982
+ const start = new Date(end);
2983
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
2984
+ const [rows, activityRows] = await Promise.all([
2985
+ directoryRows(getDb()),
2986
+ directoryActivityRows(getDb(), start)
2987
+ ]);
2988
+ const locations = [];
2989
+ const privateActivity = {
2990
+ ...emptyMetrics(),
2991
+ label: "Private activity"
2992
+ };
2993
+ for (const row of rows) {
2994
+ const location = locationFromAggregate(row);
2995
+ if (location) locations.push(location);
2996
+ else addMetrics(privateActivity, row);
2997
+ }
2998
+ return {
2999
+ activityDays: directoryActivityDays(activityRows, nowMs),
3000
+ generatedAt: new Date(nowMs).toISOString(),
3001
+ locations: locations.sort(
3002
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.label.localeCompare(right.label)
3003
+ ),
3004
+ privateActivity,
3005
+ source: "conversation_index",
3006
+ windowEnd: end.toISOString(),
3007
+ windowStart: start.toISOString()
3008
+ };
3009
+ }
3010
+ async function recentLocationRows(db, locationId) {
3011
+ return db.select({
3012
+ channelName: juniorConversations.channelName,
3013
+ conversationId: juniorConversations.conversationId,
3014
+ createdAt: juniorConversations.createdAt,
3015
+ destinationId: juniorDestinations.id,
3016
+ durationMs: juniorConversations.durationMs,
3017
+ email: sql7`COALESCE(
3018
+ ${juniorUsers.primaryEmailNormalized},
3019
+ ${juniorIdentities.email}
3020
+ )`,
3021
+ executionStatus: juniorConversations.executionStatus,
3022
+ executionUpdatedAt: juniorConversations.executionUpdatedAt,
3023
+ fullName: juniorUsers.displayName,
3024
+ handle: juniorIdentities.handle,
3025
+ lastActivityAt: juniorConversations.lastActivityAt,
3026
+ providerSubjectId: sql7`CASE
3027
+ WHEN ${juniorIdentities.provider} = 'slack'
3028
+ THEN ${juniorIdentities.providerSubjectId}
3029
+ ELSE NULL
3030
+ END`,
3031
+ source: juniorConversations.source,
3032
+ title: juniorConversations.title,
3033
+ updatedAt: juniorConversations.updatedAt,
3034
+ usage: juniorConversations.usage
3035
+ }).from(juniorConversations).innerJoin(
3036
+ juniorDestinations,
3037
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3038
+ ).leftJoin(
3039
+ juniorIdentities,
3040
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
3041
+ ).leftJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(
3042
+ and9(
3043
+ publicLocationWhere(locationId),
3044
+ isNull5(juniorConversations.archivedAt)
3045
+ )
3046
+ ).orderBy(
3047
+ desc3(juniorConversations.lastActivityAt),
3048
+ asc3(juniorConversations.conversationId)
3049
+ ).limit(RECENT_LIMIT);
3050
+ }
3051
+ async function readLocationDetailFromSql(locationId, options = {}) {
3052
+ const nowMs = Date.now();
3053
+ const end = new Date(nowMs);
3054
+ end.setUTCHours(0, 0, 0, 0);
3055
+ const start = new Date(end);
3056
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
3057
+ const where = publicLocationWhere(locationId);
3058
+ const activityDate = sql7`TO_CHAR(
3059
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3060
+ 'YYYY-MM-DD'
3061
+ )`;
3062
+ const [locationRows, dayRows, actorRows, recentRows] = await Promise.all([
3063
+ getDb().select({
3064
+ ...locationColumns(),
3065
+ ...treeAggregateColumns2,
3066
+ ...conversationRangeColumns()
3067
+ }).from(juniorConversations).innerJoin(
3068
+ treeConversation2,
3069
+ eq10(
3070
+ treeConversation2.rootConversationId,
3071
+ juniorConversations.conversationId
3072
+ )
3073
+ ).innerJoin(
3074
+ juniorDestinations,
3075
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3076
+ ).where(where).groupBy(...locationGroupBy()),
3077
+ getDb().select({
3078
+ date: activityDate,
3079
+ ...treeAggregateColumns2
3080
+ }).from(juniorConversations).innerJoin(
3081
+ treeConversation2,
3082
+ eq10(
3083
+ treeConversation2.rootConversationId,
3084
+ juniorConversations.conversationId
3085
+ )
3086
+ ).innerJoin(
3087
+ juniorDestinations,
3088
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3089
+ ).where(and9(where, gte2(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
3090
+ getDb().select({
3091
+ actorIdentityId: juniorConversations.actorIdentityId,
3092
+ email: juniorUsers.primaryEmailNormalized,
3093
+ fullName: juniorUsers.displayName,
3094
+ handle: juniorIdentities.handle,
3095
+ identityEmail: juniorIdentities.email,
3096
+ identityProvider: juniorIdentities.provider,
3097
+ providerSubjectId: juniorIdentities.providerSubjectId,
3098
+ ...treeAggregateColumns2
3099
+ }).from(juniorConversations).innerJoin(
3100
+ treeConversation2,
3101
+ eq10(
3102
+ treeConversation2.rootConversationId,
3103
+ juniorConversations.conversationId
3104
+ )
3105
+ ).innerJoin(
3106
+ juniorDestinations,
3107
+ eq10(juniorDestinations.id, juniorConversations.destinationId)
3108
+ ).leftJoin(
3109
+ juniorIdentities,
3110
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
3111
+ ).leftJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
3112
+ juniorConversations.actorIdentityId,
3113
+ juniorUsers.primaryEmailNormalized,
3114
+ juniorUsers.displayName,
3115
+ juniorIdentities.handle,
3116
+ juniorIdentities.email,
3117
+ juniorIdentities.provider,
3118
+ juniorIdentities.providerSubjectId
3119
+ ),
3120
+ recentLocationRows(getDb(), locationId)
3121
+ ]);
3122
+ const locationRow = locationRows[0];
3123
+ const location = locationRow ? locationFromAggregate(locationRow) : void 0;
3124
+ if (!location) return void 0;
3125
+ const days = /* @__PURE__ */ new Map();
3126
+ for (const row of dayRows) {
3127
+ days.set(row.date, {
3128
+ active: row.active,
3129
+ conversations: row.conversations,
3130
+ date: row.date,
3131
+ durationMs: row.durationMs,
3132
+ failed: row.failed,
3133
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3489
3134
  });
3490
- return page ? jsonResponse(pluginUserPageContentSchema2, page) : jsonResponse(
3491
- apiErrorSchema,
3492
- { error: "User page not found." },
3493
- { status: 404 }
3135
+ }
3136
+ const actors = [];
3137
+ for (const row of actorRows) {
3138
+ if (!row.actorIdentityId) continue;
3139
+ const actor = {
3140
+ ...row.email ?? row.identityEmail ? { email: row.email ?? row.identityEmail ?? void 0 } : {},
3141
+ ...row.fullName ? { fullName: row.fullName } : {},
3142
+ ...row.identityProvider === "slack" && row.providerSubjectId ? { slackUserId: row.providerSubjectId } : {},
3143
+ ...row.handle ? { slackUserName: row.handle } : {}
3144
+ };
3145
+ if (!actor.email && !actor.slackUserId) continue;
3146
+ const item = emptyActor(actor);
3147
+ addMetrics(item, row);
3148
+ actors.push(item);
3149
+ }
3150
+ const activity = activityDays(days, nowMs, ACTIVITY_DAYS);
3151
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
3152
+ const [accessByConversation, metricsByRoot] = await Promise.all([
3153
+ readConversationAccessFromSql(
3154
+ getDb(),
3155
+ recentConversationIds,
3156
+ options.verifiedViewerEmail
3157
+ ),
3158
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
3159
+ ]);
3160
+ return {
3161
+ ...location,
3162
+ activityDays: activity,
3163
+ actors: actors.sort(
3164
+ (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
3165
+ ),
3166
+ generatedAt: new Date(nowMs).toISOString(),
3167
+ recentConversations: recentRows.map(
3168
+ (row) => summaryFromRow(row, {
3169
+ access: accessByConversation.get(row.conversationId),
3170
+ metrics: metricsByRoot.get(row.conversationId)
3171
+ })
3172
+ ),
3173
+ source: "conversation_index",
3174
+ windowEnd: end.toISOString(),
3175
+ windowStart: activity[0] ? `${activity[0].date}T00:00:00.000Z` : end.toISOString()
3176
+ };
3177
+ }
3178
+
3179
+ // src/api/locations/detail.ts
3180
+ async function readLocationDetail(locationId, options = {}) {
3181
+ const report = await readLocationDetailFromSql(locationId, options);
3182
+ return report ? locationDetailReportSchema.parse(report) : void 0;
3183
+ }
3184
+ var detail_default2 = defineApiRoute({
3185
+ method: "get",
3186
+ path: "/:locationId",
3187
+ responseSchema: locationDetailReportSchema,
3188
+ handler: async (c) => {
3189
+ const { locationId } = parseParams(locationParamsSchema, c.req.param());
3190
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
3191
+ const report = await readLocationDetail(
3192
+ locationId,
3193
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
3494
3194
  );
3495
- });
3195
+ if (!report) throwApiError(404, "Location not found.");
3196
+ return report;
3197
+ }
3198
+ });
3199
+
3200
+ // src/api/locations/list.ts
3201
+ async function readLocationDirectory() {
3202
+ return locationDirectoryReportSchema.parse(
3203
+ await readLocationDirectoryFromSql()
3204
+ );
3205
+ }
3206
+ var list_default2 = defineApiRoute({
3207
+ method: "get",
3208
+ path: "/",
3209
+ responseSchema: locationDirectoryReportSchema,
3210
+ handler: readLocationDirectory
3211
+ });
3212
+
3213
+ // src/api/locations/routes.ts
3214
+ var routes2 = [list_default2, detail_default2];
3215
+ function createLocationRoutes() {
3216
+ const app = new Hono2();
3217
+ registerApiRoutes(app, routes2);
3496
3218
  return app;
3497
3219
  }
3498
3220
 
3499
- // src/api/tasks/routes.ts
3500
- import { Hono as Hono6 } from "hono";
3221
+ // src/api/people/routes.ts
3222
+ import { Hono as Hono3 } from "hono";
3501
3223
 
3502
- // src/chat/tasks/read.ts
3503
- import { and as and14, eq as eq15, or as or3 } from "drizzle-orm";
3224
+ // src/api/people/list.query.ts
3225
+ import { and as and11, eq as eq12, gte as gte3, sql as sql9 } from "drizzle-orm";
3504
3226
 
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";
3227
+ // src/api/people/shared.ts
3228
+ 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";
3229
+ import { alias as alias6 } from "drizzle-orm/pg-core";
3230
+ var RECENT_LIMIT2 = 25;
3231
+ var ACTIVITY_DAYS2 = 365;
3232
+ var peopleTreeConversation = alias6(
3233
+ juniorConversations,
3234
+ "people_tree_conversation"
3235
+ );
3236
+ var peopleRootConversation = alias6(
3237
+ juniorConversations,
3238
+ "people_root_conversation"
3239
+ );
3240
+ var peopleRootIdentity = alias6(juniorIdentities, "people_root_identity");
3241
+ var peopleTreeAggregateColumns = conversationAggregateColumns({
3242
+ metrics: peopleTreeConversation,
3243
+ roots: juniorConversations
3244
+ });
3245
+ function peopleTreeMetricsJoin() {
3246
+ const rootOwnedByActor = getDb().select({ conversationId: peopleRootConversation.conversationId }).from(peopleRootConversation).innerJoin(
3247
+ peopleRootIdentity,
3248
+ eq11(peopleRootIdentity.id, peopleRootConversation.actorIdentityId)
3249
+ ).where(
3250
+ and10(
3251
+ eq11(
3252
+ peopleRootConversation.conversationId,
3253
+ juniorConversations.rootConversationId
3254
+ ),
3255
+ isNull6(peopleRootConversation.parentConversationId),
3256
+ eq11(
3257
+ peopleRootConversation.rootConversationId,
3258
+ peopleRootConversation.conversationId
3259
+ ),
3260
+ eq11(
3261
+ peopleRootIdentity.emailNormalized,
3262
+ juniorUsers.primaryEmailNormalized
3263
+ ),
3264
+ eq11(peopleRootIdentity.emailVerified, true)
3265
+ )
3266
+ );
3267
+ return and10(
3268
+ eq11(
3269
+ peopleTreeConversation.rootConversationId,
3270
+ juniorConversations.rootConversationId
3271
+ ),
3272
+ or3(
3273
+ eq11(
3274
+ juniorConversations.rootConversationId,
3275
+ juniorConversations.conversationId
3276
+ ),
3277
+ and10(
3278
+ eq11(
3279
+ peopleTreeConversation.conversationId,
3280
+ juniorConversations.conversationId
3281
+ ),
3282
+ notExists(rootOwnedByActor)
3283
+ )
3284
+ )
3285
+ );
3286
+ }
3287
+ function normalizeEmail(email) {
3288
+ const normalized = email?.trim().toLowerCase();
3289
+ return normalized || void 0;
3290
+ }
3291
+ function emptyTotals() {
3292
+ return {
3293
+ active: 0,
3294
+ activeDays: 0,
3295
+ conversations: 0,
3296
+ durationMs: 0,
3297
+ failed: 0
3298
+ };
3299
+ }
3300
+ function emptyActivityDay(date) {
3301
+ return {
3302
+ active: 0,
3303
+ conversations: 0,
3304
+ date,
3305
+ durationMs: 0,
3306
+ failed: 0
3307
+ };
3308
+ }
3309
+ function activityDays2(days, nowMs) {
3310
+ const items = [];
3311
+ const end = new Date(nowMs);
3312
+ end.setUTCHours(0, 0, 0, 0);
3313
+ const start = new Date(end);
3314
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3315
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
3316
+ const date = cursor.toISOString().slice(0, 10);
3317
+ items.push(days.get(date) ?? emptyActivityDay(date));
3517
3318
  }
3518
- };
3519
- var PersonalScheduledTaskNotFoundError = class extends Error {
3520
- constructor() {
3521
- super("Scheduled task was not found.");
3522
- this.name = "PersonalScheduledTaskNotFoundError";
3319
+ return items;
3320
+ }
3321
+ function statsItems2(map) {
3322
+ return [...map.values()].sort(
3323
+ (left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
3324
+ );
3325
+ }
3326
+ function verifiedActorWhere(email) {
3327
+ const normalizedEmail = normalizeEmail(email);
3328
+ return and10(
3329
+ eq11(juniorIdentities.provider, "slack"),
3330
+ eq11(juniorIdentities.emailVerified, true),
3331
+ sql8`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
3332
+ normalizedEmail ? eq11(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0
3333
+ );
3334
+ }
3335
+ async function recentActorRows(email) {
3336
+ return getDb().select({
3337
+ channelName: juniorConversations.channelName,
3338
+ conversationId: juniorConversations.conversationId,
3339
+ createdAt: juniorConversations.createdAt,
3340
+ destinationId: juniorDestinations.id,
3341
+ durationMs: juniorConversations.durationMs,
3342
+ email: juniorUsers.primaryEmailNormalized,
3343
+ executionStatus: juniorConversations.executionStatus,
3344
+ executionUpdatedAt: juniorConversations.executionUpdatedAt,
3345
+ fullName: juniorUsers.displayName,
3346
+ handle: juniorIdentities.handle,
3347
+ lastActivityAt: juniorConversations.lastActivityAt,
3348
+ providerSubjectId: juniorIdentities.providerSubjectId,
3349
+ source: juniorConversations.source,
3350
+ title: juniorConversations.title,
3351
+ updatedAt: juniorConversations.updatedAt,
3352
+ usage: juniorConversations.usage
3353
+ }).from(juniorConversations).innerJoin(
3354
+ juniorIdentities,
3355
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
3356
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(
3357
+ juniorDestinations,
3358
+ eq11(juniorDestinations.id, juniorConversations.destinationId)
3359
+ ).where(
3360
+ and10(verifiedActorWhere(email), isNull6(juniorConversations.archivedAt))
3361
+ ).orderBy(
3362
+ desc4(juniorConversations.lastActivityAt),
3363
+ asc4(juniorConversations.conversationId)
3364
+ ).limit(RECENT_LIMIT2);
3365
+ }
3366
+
3367
+ // src/api/people/list.query.ts
3368
+ var DIRECTORY_ACTIVITY_DAYS = 90;
3369
+ function activityWindow(nowMs) {
3370
+ const end = new Date(nowMs);
3371
+ end.setUTCHours(0, 0, 0, 0);
3372
+ const start = new Date(end);
3373
+ start.setUTCDate(start.getUTCDate() - (DIRECTORY_ACTIVITY_DAYS - 1));
3374
+ return { end, start };
3375
+ }
3376
+ function directoryActivityDays2(rows, nowMs) {
3377
+ const days = new Map(rows.map((row) => [row.date, row]));
3378
+ const { end, start } = activityWindow(nowMs);
3379
+ const items = [];
3380
+ for (const cursor = new Date(start); cursor.getTime() <= end.getTime(); cursor.setUTCDate(cursor.getUTCDate() + 1)) {
3381
+ const date = cursor.toISOString().slice(0, 10);
3382
+ items.push(days.get(date) ?? { activePeople: 0, conversations: 0, date });
3523
3383
  }
3524
- };
3525
- function normalizeQuery(query) {
3526
- return query?.trim().toLowerCase() || void 0;
3384
+ return items;
3527
3385
  }
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"))
3386
+ async function readPeopleListFromSql() {
3387
+ const nowMs = Date.now();
3388
+ const { end, start } = activityWindow(nowMs);
3389
+ const activityDate = sql9`TO_CHAR(
3390
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3391
+ 'YYYY-MM-DD'
3392
+ )`;
3393
+ const [rows, activityRows] = await Promise.all([
3394
+ getDb().select({
3395
+ email: juniorUsers.primaryEmailNormalized,
3396
+ fullName: juniorUsers.displayName,
3397
+ slackUserId: sql9`MAX(${juniorIdentities.providerSubjectId})`,
3398
+ slackUserName: sql9`MAX(${juniorIdentities.handle})`,
3399
+ activeDays: conversationActiveDaysColumn(),
3400
+ ...peopleTreeAggregateColumns,
3401
+ ...conversationRangeColumns()
3402
+ }).from(juniorConversations).innerJoin(
3403
+ juniorIdentities,
3404
+ eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3405
+ ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, peopleTreeMetricsJoin()).where(verifiedActorWhere()).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
3406
+ getDb().select({
3407
+ activePeople: sql9`COUNT(DISTINCT ${juniorUsers.id})::int`,
3408
+ conversations: sql9`COUNT(*)::int`,
3409
+ date: activityDate
3410
+ }).from(juniorConversations).innerJoin(
3411
+ juniorIdentities,
3412
+ eq12(juniorIdentities.id, juniorConversations.actorIdentityId)
3413
+ ).innerJoin(juniorUsers, eq12(juniorUsers.id, juniorIdentities.userId)).where(
3414
+ and11(
3415
+ verifiedActorWhere(),
3416
+ gte3(juniorConversations.lastActivityAt, start)
3417
+ )
3418
+ ).groupBy(activityDate)
3419
+ ]);
3420
+ const people = rows.map((row) => {
3421
+ const actor = {
3422
+ email: row.email,
3423
+ ...row.fullName ? { fullName: row.fullName } : {},
3424
+ ...row.slackUserId ? { slackUserId: row.slackUserId } : {},
3425
+ ...row.slackUserName ? { slackUserName: row.slackUserName } : {}
3426
+ };
3427
+ return {
3428
+ active: row.active,
3429
+ activeDays: row.activeDays,
3430
+ conversations: row.conversations,
3431
+ durationMs: row.durationMs,
3432
+ failed: row.failed,
3433
+ firstSeenAt: row.firstSeenAt.toISOString(),
3434
+ lastSeenAt: row.lastSeenAt.toISOString(),
3435
+ actor,
3436
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3437
+ };
3438
+ });
3439
+ return {
3440
+ activityDays: directoryActivityDays2(activityRows, nowMs),
3441
+ generatedAt: new Date(nowMs).toISOString(),
3442
+ people: people.sort(
3443
+ (left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt) || right.conversations - left.conversations || left.actor.email.localeCompare(right.actor.email)
3444
+ ),
3445
+ source: "conversation_index",
3446
+ windowEnd: end.toISOString(),
3447
+ windowStart: start.toISOString()
3448
+ };
3449
+ }
3450
+
3451
+ // src/api/people/list.ts
3452
+ async function readPeopleList() {
3453
+ return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
3454
+ }
3455
+ var list_default3 = defineApiRoute({
3456
+ method: "get",
3457
+ path: "/",
3458
+ responseSchema: actorDirectoryReportSchema,
3459
+ handler: readPeopleList
3460
+ });
3461
+
3462
+ // src/api/people/profile.query.ts
3463
+ import { and as and12, eq as eq13, gte as gte4, sql as sql10 } from "drizzle-orm";
3464
+ function emptyProfile(email, nowMs) {
3465
+ const end = new Date(nowMs);
3466
+ end.setUTCHours(0, 0, 0, 0);
3467
+ const start = new Date(end);
3468
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3469
+ return {
3470
+ activityDays: activityDays2(/* @__PURE__ */ new Map(), nowMs),
3471
+ generatedAt: new Date(nowMs).toISOString(),
3472
+ locations: [],
3473
+ recentConversations: [],
3474
+ actor: { email },
3475
+ source: "conversation_index",
3476
+ surfaces: [],
3477
+ totals: emptyTotals(),
3478
+ windowEnd: end.toISOString(),
3479
+ windowStart: start.toISOString()
3480
+ };
3481
+ }
3482
+ function addAggregate2(map, label, row) {
3483
+ const item = map.get(label) ?? {
3484
+ active: 0,
3485
+ conversations: 0,
3486
+ durationMs: 0,
3487
+ failed: 0,
3488
+ label
3489
+ };
3490
+ item.active += row.active;
3491
+ item.conversations += row.conversations;
3492
+ item.durationMs += row.durationMs;
3493
+ item.failed += row.failed;
3494
+ if (row.tokens !== null) item.tokens = (item.tokens ?? 0) + row.tokens;
3495
+ map.set(label, item);
3496
+ }
3497
+ function surfaceExpression() {
3498
+ return sql10`CASE
3499
+ WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
3500
+ THEN ${juniorConversations.source}
3501
+ WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
3502
+ WHEN ${juniorConversations.conversationId} LIKE 'scheduler:%' THEN 'scheduler'
3503
+ WHEN ${juniorConversations.conversationId} LIKE 'api:%' THEN 'api'
3504
+ ELSE 'internal'
3505
+ END`;
3506
+ }
3507
+ function locationLabel2(row) {
3508
+ if (row.surface !== "slack")
3509
+ return surfaceLabel2(row.surface);
3510
+ if (row.destinationVisibility !== "public") return "Private Conversation";
3511
+ return slackLocationLabel({
3512
+ channel: row.channel || void 0,
3513
+ channelName: row.channelName ?? void 0
3514
+ }) ?? "Conversation";
3515
+ }
3516
+ async function readPeopleProfileFromSql(email, options = {}) {
3517
+ const nowMs = Date.now();
3518
+ const normalizedEmail = normalizeEmail(email);
3519
+ if (!normalizedEmail) return emptyProfile("", nowMs);
3520
+ const end = new Date(nowMs);
3521
+ end.setUTCHours(0, 0, 0, 0);
3522
+ const start = new Date(end);
3523
+ start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
3524
+ const where = verifiedActorWhere(normalizedEmail);
3525
+ const metricsJoin = peopleTreeMetricsJoin();
3526
+ const surface = surfaceExpression();
3527
+ const activityDate = sql10`TO_CHAR(
3528
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3529
+ 'YYYY-MM-DD'
3530
+ )`;
3531
+ const channel = sql10`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
3532
+ const [totalsRows, dayRows, locationRows, surfaceRows, recentRows] = await Promise.all([
3533
+ getDb().select({
3534
+ email: juniorUsers.primaryEmailNormalized,
3535
+ fullName: juniorUsers.displayName,
3536
+ slackUserId: sql10`MAX(${juniorIdentities.providerSubjectId})`,
3537
+ slackUserName: sql10`MAX(${juniorIdentities.handle})`,
3538
+ activeDays: conversationActiveDaysColumn(),
3539
+ ...peopleTreeAggregateColumns
3540
+ }).from(juniorConversations).innerJoin(
3541
+ juniorIdentities,
3542
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3543
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
3544
+ getDb().select({
3545
+ date: activityDate,
3546
+ ...peopleTreeAggregateColumns
3547
+ }).from(juniorConversations).innerJoin(
3548
+ juniorIdentities,
3549
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3550
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(and12(where, gte4(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
3551
+ getDb().select({
3552
+ channel,
3553
+ channelName: juniorConversations.channelName,
3554
+ destinationVisibility: juniorDestinations.visibility,
3555
+ surface,
3556
+ ...peopleTreeAggregateColumns
3557
+ }).from(juniorConversations).innerJoin(
3558
+ juniorIdentities,
3559
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3560
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).leftJoin(
3561
+ juniorDestinations,
3562
+ eq13(juniorDestinations.id, juniorConversations.destinationId)
3563
+ ).where(where).groupBy(
3564
+ channel,
3565
+ juniorConversations.channelName,
3566
+ juniorDestinations.visibility,
3567
+ surface
3568
+ ),
3569
+ getDb().select({ surface, ...peopleTreeAggregateColumns }).from(juniorConversations).innerJoin(
3570
+ juniorIdentities,
3571
+ eq13(juniorIdentities.id, juniorConversations.actorIdentityId)
3572
+ ).innerJoin(juniorUsers, eq13(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(surface),
3573
+ recentActorRows(normalizedEmail)
3574
+ ]);
3575
+ const totalsRow = totalsRows[0];
3576
+ if (!totalsRow) return emptyProfile(normalizedEmail, nowMs);
3577
+ const actor = {
3578
+ email: totalsRow.email,
3579
+ ...totalsRow.fullName ? { fullName: totalsRow.fullName } : {},
3580
+ ...totalsRow.slackUserId ? { slackUserId: totalsRow.slackUserId } : {},
3581
+ ...totalsRow.slackUserName ? { slackUserName: totalsRow.slackUserName } : {}
3582
+ };
3583
+ const days = /* @__PURE__ */ new Map();
3584
+ for (const row of dayRows) {
3585
+ days.set(row.date, {
3586
+ active: row.active,
3587
+ conversations: row.conversations,
3588
+ ...row.costUsd !== null ? { costUsd: row.costUsd } : {},
3589
+ date: row.date,
3590
+ durationMs: row.durationMs,
3591
+ failed: row.failed,
3592
+ ...row.tokens !== null ? { tokens: row.tokens } : {}
3593
+ });
3594
+ }
3595
+ const locations = /* @__PURE__ */ new Map();
3596
+ for (const row of locationRows) {
3597
+ addAggregate2(locations, locationLabel2(row), row);
3598
+ }
3599
+ const surfaces = /* @__PURE__ */ new Map();
3600
+ for (const row of surfaceRows) {
3601
+ addAggregate2(
3602
+ surfaces,
3603
+ surfaceLabel2(row.surface),
3604
+ row
3533
3605
  );
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
3606
  }
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));
3607
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
3608
+ const [accessByConversation, metricsByRoot] = await Promise.all([
3609
+ readConversationAccessFromSql(
3610
+ getDb(),
3611
+ recentConversationIds,
3612
+ options.verifiedViewerEmail
3613
+ ),
3614
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
3615
+ ]);
3555
3616
  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
- });
3617
+ activityDays: activityDays2(days, nowMs),
3618
+ generatedAt: new Date(nowMs).toISOString(),
3619
+ locations: statsItems2(locations),
3620
+ recentConversations: recentRows.map(
3621
+ (row) => summaryFromRow(row, {
3622
+ access: accessByConversation.get(row.conversationId),
3623
+ metrics: metricsByRoot.get(row.conversationId)
3624
+ })
3625
+ ),
3626
+ actor,
3627
+ source: "conversation_index",
3628
+ surfaces: statsItems2(surfaces),
3629
+ totals: {
3630
+ active: totalsRow.active,
3631
+ activeDays: totalsRow.activeDays,
3632
+ conversations: totalsRow.conversations,
3633
+ durationMs: totalsRow.durationMs,
3634
+ failed: totalsRow.failed,
3635
+ ...totalsRow.tokens !== null ? { tokens: totalsRow.tokens } : {}
3568
3636
  },
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
- }
3637
+ windowEnd: end.toISOString(),
3638
+ windowStart: start.toISOString()
3584
3639
  };
3585
3640
  }
3586
3641
 
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;
3642
+ // src/api/people/profile.ts
3643
+ async function readPeopleProfile(email, options = {}) {
3644
+ return actorProfileReportSchema.parse(
3645
+ await readPeopleProfileFromSql(email, options)
3646
+ );
3593
3647
  }
3594
- function creatorKey(teamId, slackUserId) {
3595
- return `${teamId}:${slackUserId}`;
3648
+ var profile_default = defineApiRoute({
3649
+ method: "get",
3650
+ path: "/:email",
3651
+ responseSchema: actorProfileReportSchema,
3652
+ handler: async (c) => {
3653
+ const { email } = parseParams(personParamsSchema, c.req.param());
3654
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
3655
+ return readPeopleProfile(
3656
+ email,
3657
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
3658
+ );
3659
+ }
3660
+ });
3661
+
3662
+ // src/api/people/spend.query.ts
3663
+ import { and as and13, eq as eq14, gte as gte5, lte as lte2, sql as sql11 } from "drizzle-orm";
3664
+ var SEVEN_DAY_OFFSET = 6;
3665
+ var THIRTY_DAY_OFFSET = 29;
3666
+ function addUsd2(current, next) {
3667
+ return Math.round((current + next) * 1e12) / 1e12;
3596
3668
  }
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
- )
3669
+ function spendWindow(nowMs) {
3670
+ const end = new Date(nowMs);
3671
+ const currentDay = new Date(end);
3672
+ currentDay.setUTCHours(0, 0, 0, 0);
3673
+ const sevenDayStart = new Date(currentDay);
3674
+ sevenDayStart.setUTCDate(sevenDayStart.getUTCDate() - SEVEN_DAY_OFFSET);
3675
+ const thirtyDayStart = new Date(currentDay);
3676
+ thirtyDayStart.setUTCDate(thirtyDayStart.getUTCDate() - THIRTY_DAY_OFFSET);
3677
+ return { end, sevenDayStart, thirtyDayStart };
3678
+ }
3679
+ async function readPersonalSpendFromSql(email, nowMs = Date.now()) {
3680
+ const normalizedEmail = normalizeEmail(email);
3681
+ const { end, sevenDayStart, thirtyDayStart } = spendWindow(nowMs);
3682
+ const activityDate = sql11`TO_CHAR(
3683
+ ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
3684
+ 'YYYY-MM-DD'
3685
+ )`;
3686
+ const metricsJoin = peopleTreeMetricsJoin();
3687
+ const rows = normalizedEmail ? await getDb().select({
3688
+ costUsd: peopleTreeAggregateColumns.costUsd,
3689
+ date: activityDate
3690
+ }).from(juniorConversations).innerJoin(
3691
+ juniorIdentities,
3692
+ eq14(juniorIdentities.id, juniorConversations.actorIdentityId)
3693
+ ).innerJoin(juniorUsers, eq14(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(
3694
+ and13(
3695
+ verifiedActorWhere(normalizedEmail),
3696
+ gte5(juniorConversations.lastActivityAt, thirtyDayStart),
3697
+ lte2(juniorConversations.lastActivityAt, end)
3626
3698
  )
3627
- );
3628
- return new Map(
3629
- rows.map((row) => [creatorKey(row.teamId, row.slackUserId), row.email])
3630
- );
3699
+ ).groupBy(activityDate) : [];
3700
+ const sevenDayStartDate = sevenDayStart.toISOString().slice(0, 10);
3701
+ let sevenDaysUsd = 0;
3702
+ let thirtyDaysUsd = 0;
3703
+ for (const row of rows) {
3704
+ if (row.costUsd === null) continue;
3705
+ thirtyDaysUsd = addUsd2(thirtyDaysUsd, row.costUsd);
3706
+ if (row.date >= sevenDayStartDate) {
3707
+ sevenDaysUsd = addUsd2(sevenDaysUsd, row.costUsd);
3708
+ }
3709
+ }
3710
+ return {
3711
+ generatedAt: new Date(nowMs).toISOString(),
3712
+ sevenDaysUsd,
3713
+ source: "conversation_index",
3714
+ thirtyDaysUsd,
3715
+ windowEnd: end.toISOString(),
3716
+ windowStart: thirtyDayStart.toISOString()
3717
+ };
3631
3718
  }
3632
- function destinationKey(destination) {
3633
- return `${destination.teamId}:${destination.channelId}`;
3719
+
3720
+ // src/api/people/spend.ts
3721
+ var PERSONAL_SPEND_CACHE_TTL_MS = 5 * 6e4;
3722
+ function createPersonalSpendRoute() {
3723
+ const cache = /* @__PURE__ */ new Map();
3724
+ async function read(email) {
3725
+ const normalizedEmail = email.trim().toLowerCase();
3726
+ const nowMs = Date.now();
3727
+ const cached = cache.get(normalizedEmail);
3728
+ if (cached && cached.expiresAtMs > nowMs) {
3729
+ return cached.report;
3730
+ }
3731
+ cache.delete(normalizedEmail);
3732
+ const report = readPersonalSpendFromSql(normalizedEmail, nowMs);
3733
+ const entry = {
3734
+ expiresAtMs: nowMs + PERSONAL_SPEND_CACHE_TTL_MS,
3735
+ report
3736
+ };
3737
+ cache.set(normalizedEmail, entry);
3738
+ try {
3739
+ return await report;
3740
+ } catch (error) {
3741
+ if (cache.get(normalizedEmail) === entry) cache.delete(normalizedEmail);
3742
+ throw error;
3743
+ }
3744
+ }
3745
+ return defineApiRoute({
3746
+ method: "get",
3747
+ path: "/me/spend",
3748
+ responseSchema: personalSpendReportSchema,
3749
+ handler: async (context) => {
3750
+ const email = context.get("verifiedViewerEmail");
3751
+ if (!email) throwApiError(403, "Verified viewer email required.");
3752
+ return read(email);
3753
+ }
3754
+ });
3634
3755
  }
3635
- function displayText(value, fallback) {
3636
- return value.trim() || fallback;
3756
+
3757
+ // src/api/people/routes.ts
3758
+ function createPeopleRoutes() {
3759
+ const app = new Hono3();
3760
+ const routes5 = [
3761
+ createPersonalSpendRoute(),
3762
+ list_default3,
3763
+ profile_default
3764
+ ];
3765
+ registerApiRoutes(app, routes5);
3766
+ return app;
3637
3767
  }
3638
- function taskDisplayTitle(title, instruction, fallback) {
3639
- const stored = title?.trim();
3640
- if (stored) return stored;
3641
- return fallbackShortTitle(instruction, fallback);
3768
+
3769
+ // src/api/plugin-reports.ts
3770
+ var plugin_reports_default = defineApiRoute({
3771
+ method: "get",
3772
+ path: "/api/plugin-reports",
3773
+ responseSchema: pluginOperationalReportFeedSchema,
3774
+ handler: readPluginOperationalReportFeed
3775
+ });
3776
+
3777
+ // src/api/personal-tokens/routes.ts
3778
+ import { Hono as Hono4 } from "hono";
3779
+
3780
+ // src/personal-tokens/store.ts
3781
+ import { createHash, randomBytes, randomUUID } from "crypto";
3782
+ import { and as and14, desc as desc5, eq as eq15, gt, isNull as isNull7 } from "drizzle-orm";
3783
+ var TOKEN_PREFIX = "jr_pat_";
3784
+ var TOKEN_SUFFIX_LENGTH = 8;
3785
+ var DEFAULT_TOKEN_LIFETIME_MS = 90 * 24 * 60 * 60 * 1e3;
3786
+ function normalizeEmail2(email) {
3787
+ return email.trim().toLowerCase();
3642
3788
  }
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
- );
3789
+ function hashToken(token2) {
3790
+ return createHash("sha256").update(token2, "utf8").digest("hex");
3681
3791
  }
3682
- function executionSummaryFields(stats) {
3792
+ function metadata(row) {
3683
3793
  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
3794
+ id: row.id,
3795
+ name: row.name,
3796
+ tokenSuffix: row.tokenSuffix,
3797
+ createdAt: row.createdAt.toISOString(),
3798
+ expiresAt: row.expiresAt.toISOString(),
3799
+ lastUsedAt: row.lastUsedAt?.toISOString() ?? null
3688
3800
  };
3689
3801
  }
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")
3802
+ async function createPersonalToken(args) {
3803
+ const now = args.now ?? /* @__PURE__ */ new Date();
3804
+ const token2 = `${TOKEN_PREFIX}${randomBytes(32).toString("base64url")}`;
3805
+ const row = {
3806
+ id: randomUUID(),
3807
+ ownerEmailNormalized: normalizeEmail2(args.email),
3808
+ name: args.name.trim(),
3809
+ tokenHash: hashToken(token2),
3810
+ tokenSuffix: token2.slice(-TOKEN_SUFFIX_LENGTH),
3811
+ createdAt: now,
3812
+ expiresAt: new Date(now.getTime() + DEFAULT_TOKEN_LIFETIME_MS)
3715
3813
  };
3814
+ await getDb().insert(juniorApiTokens).values(row);
3815
+ return { ...metadata({ ...row, lastUsedAt: null, revokedAt: null }), token: token2 };
3716
3816
  }
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()
3817
+ async function listPersonalTokens(email, now = /* @__PURE__ */ new Date()) {
3818
+ const rows = await getDb().select().from(juniorApiTokens).where(
3819
+ and14(
3820
+ eq15(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(email)),
3821
+ isNull7(juniorApiTokens.revokedAt),
3822
+ gt(juniorApiTokens.expiresAt, now)
3741
3823
  )
3742
- };
3824
+ ).orderBy(desc5(juniorApiTokens.createdAt));
3825
+ return rows.map(metadata);
3743
3826
  }
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))
3827
+ async function revokePersonalToken(args) {
3828
+ const rows = await getDb().update(juniorApiTokens).set({ revokedAt: args.now ?? /* @__PURE__ */ new Date() }).where(
3829
+ and14(
3830
+ eq15(juniorApiTokens.id, args.id),
3831
+ eq15(juniorApiTokens.ownerEmailNormalized, normalizeEmail2(args.email)),
3832
+ isNull7(juniorApiTokens.revokedAt)
3748
3833
  )
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
- };
3834
+ ).returning({ id: juniorApiTokens.id });
3835
+ return rows.length > 0;
3783
3836
  }
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
3837
+ async function authenticatePersonalToken(token2, now = /* @__PURE__ */ new Date()) {
3838
+ if (!token2.startsWith(TOKEN_PREFIX)) return void 0;
3839
+ const rows = await getDb().select({
3840
+ id: juniorApiTokens.id,
3841
+ ownerEmailNormalized: juniorApiTokens.ownerEmailNormalized
3842
+ }).from(juniorApiTokens).where(
3843
+ and14(
3844
+ eq15(juniorApiTokens.tokenHash, hashToken(token2)),
3845
+ isNull7(juniorApiTokens.revokedAt),
3846
+ gt(juniorApiTokens.expiresAt, now)
3800
3847
  )
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
- );
3848
+ ).limit(1);
3849
+ const match = rows[0];
3850
+ if (!match) return void 0;
3851
+ await getDb().update(juniorApiTokens).set({ lastUsedAt: now }).where(eq15(juniorApiTokens.id, match.id));
3852
+ return match.ownerEmailNormalized;
3818
3853
  }
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
- });
3854
+
3855
+ // src/api/personal-tokens/create.ts
3856
+ var create_default = defineApiRoute({
3857
+ method: "post",
3858
+ path: "/",
3859
+ responseSchema: createdPersonalTokenSchema,
3860
+ handler: async (c) => {
3861
+ const email = c.get("verifiedViewerEmail");
3862
+ if (!email) throwApiError(403, "Verified viewer email required.");
3863
+ let input;
3864
+ try {
3865
+ input = await c.req.json();
3866
+ } catch (error) {
3867
+ throwApiError(400, "Invalid request body.", error);
3868
+ }
3869
+ const body = parseBody(createPersonalTokenBodySchema, input);
3870
+ return createPersonalToken({ email, name: body.name });
3842
3871
  }
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
- });
3872
+ });
3873
+
3874
+ // src/api/personal-tokens/list.ts
3875
+ var list_default4 = defineApiRoute({
3876
+ method: "get",
3877
+ path: "/",
3878
+ responseSchema: personalTokenListSchema,
3879
+ handler: async (c) => {
3880
+ const email = c.get("verifiedViewerEmail");
3881
+ if (!email) throwApiError(403, "Verified viewer email required.");
3882
+ return { tokens: await listPersonalTokens(email) };
3850
3883
  }
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
3884
+ });
3885
+
3886
+ // src/api/personal-tokens/revoke.ts
3887
+ var revoke_default = defineApiRoute({
3888
+ method: "delete",
3889
+ path: "/:id",
3890
+ responseSchema: revokePersonalTokenResponseSchema,
3891
+ handler: async (c) => {
3892
+ const email = c.get("verifiedViewerEmail");
3893
+ if (!email) throwApiError(403, "Verified viewer email required.");
3894
+ const { id } = parseParams(personalTokenParamsSchema, c.req.param());
3895
+ if (!await revokePersonalToken({ email, id })) {
3896
+ throwApiError(404, "Personal API token not found.");
3897
+ }
3898
+ return { revoked: true };
3899
+ }
3900
+ });
3901
+
3902
+ // src/api/personal-tokens/routes.ts
3903
+ var routes3 = [list_default4, create_default, revoke_default];
3904
+ function createPersonalTokenRoutes() {
3905
+ const app = new Hono4();
3906
+ registerApiRoutes(app, routes3);
3907
+ return app;
3908
+ }
3909
+
3910
+ // src/api/user-pages/routes.ts
3911
+ import { Hono as Hono5 } from "hono";
3912
+ import {
3913
+ pluginUserPageContentSchema as pluginUserPageContentSchema2,
3914
+ pluginUserPageInputSchema,
3915
+ pluginUserPageLinksSchema as pluginUserPageLinksSchema2
3916
+ } from "@sentry/junior-plugin-api";
3917
+
3918
+ // src/chat/plugins/user-pages.ts
3919
+ import {
3920
+ pluginUserPageContentSchema,
3921
+ pluginUserPageLinksSchema
3922
+ } from "@sentry/junior-plugin-api";
3923
+ function readPluginUserPageLinks() {
3924
+ return pluginUserPageLinksSchema.parse(
3925
+ getPlugins().flatMap(
3926
+ (plugin) => (plugin.userPages ?? []).map((page) => ({
3927
+ description: page.description,
3928
+ id: page.id,
3929
+ label: page.label,
3930
+ navigation: page.navigation ?? "profile",
3931
+ pluginDisplayName: plugin.manifest.displayName,
3932
+ pluginName: plugin.manifest.name
3933
+ }))
3934
+ )
3856
3935
  );
3857
- const publicCandidates = candidates.filter(
3858
- (candidate) => candidate.publicToViewer
3936
+ }
3937
+ function actionBelongsToPlugin(href, pluginName) {
3938
+ const parsed = new URL(href, "http://junior.local");
3939
+ return parsed.origin === "http://junior.local" && parsed.pathname === href && parsed.pathname.startsWith(`/api/plugins/${pluginName}/`) && !parsed.search && !parsed.hash;
3940
+ }
3941
+ async function readPluginUserPage(input) {
3942
+ const plugin = getPlugins().find(
3943
+ (candidate) => candidate.manifest.name === input.pluginName
3859
3944
  );
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)
3945
+ const page = plugin?.userPages?.find(
3946
+ (candidate) => candidate.id === input.pageId
3866
3947
  );
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"
3948
+ if (!plugin || !page) return void 0;
3949
+ const viewer2 = await resolveViewerUser(input.email);
3950
+ if (!viewer2) {
3951
+ throw new Error("Authenticated viewer user could not be resolved");
3952
+ }
3953
+ const content = pluginUserPageContentSchema.parse(
3954
+ await page.read(
3955
+ {
3956
+ db: getDb(),
3957
+ log: createPluginLogger(plugin.manifest.name),
3958
+ plugin: { name: plugin.manifest.name },
3959
+ viewer: viewer2
3960
+ },
3961
+ input.query
3962
+ )
3873
3963
  );
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
- )
3964
+ if (content.records.some(
3965
+ (record) => record.actions?.some(
3966
+ (action) => !actionBelongsToPlugin(action.href, plugin.manifest.name)
3967
+ )
3968
+ )) {
3969
+ throw new Error(
3970
+ `Plugin user page "${plugin.manifest.name}/${page.id}" returned an action outside its API namespace.`
3891
3971
  );
3892
- if (candidate.kind === "scheduled") {
3893
- return scheduledTaskSummary(
3894
- candidate.task,
3895
- candidate.ownedByViewer,
3896
- destination,
3897
- scheduledStats.get(candidate.task.id),
3898
- createdByEmail
3972
+ }
3973
+ return content;
3974
+ }
3975
+
3976
+ // src/api/user-pages/routes.ts
3977
+ function createUserPageRoutes() {
3978
+ const app = new Hono5();
3979
+ app.get(
3980
+ "/",
3981
+ () => jsonResponse(pluginUserPageLinksSchema2, readPluginUserPageLinks())
3982
+ );
3983
+ app.get("/:pluginName/:pageId", async (context) => {
3984
+ const email = context.get("verifiedViewerEmail")?.trim();
3985
+ if (!email) {
3986
+ return jsonResponse(
3987
+ apiErrorSchema,
3988
+ { error: "Authentication required." },
3989
+ { status: 401 }
3899
3990
  );
3900
3991
  }
3901
- return eventTaskSummary(
3902
- candidate.task,
3903
- candidate.ownedByViewer,
3904
- destination,
3905
- eventStats.get(candidate.task.id),
3906
- createdByEmail
3992
+ const pageInput = pluginUserPageInputSchema.safeParse({
3993
+ cursor: context.req.query("cursor") || void 0,
3994
+ filter: context.req.query("filter") || void 0,
3995
+ limit: context.req.query("limit") ? Number(context.req.query("limit")) : 20,
3996
+ query: context.req.query("q") || void 0
3997
+ });
3998
+ if (!pageInput.success) {
3999
+ return jsonResponse(
4000
+ apiErrorSchema,
4001
+ { error: "Invalid user page query." },
4002
+ { status: 400 }
4003
+ );
4004
+ }
4005
+ const page = await readPluginUserPage({
4006
+ email,
4007
+ pageId: context.req.param("pageId"),
4008
+ pluginName: context.req.param("pluginName"),
4009
+ query: pageInput.data
4010
+ });
4011
+ return page ? jsonResponse(pluginUserPageContentSchema2, page) : jsonResponse(
4012
+ apiErrorSchema,
4013
+ { error: "User page not found." },
4014
+ { status: 404 }
3907
4015
  );
3908
4016
  });
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);
4017
+ return app;
3963
4018
  }
3964
4019
 
3965
4020
  // src/api/tasks/routes.ts
4021
+ import { Hono as Hono6 } from "hono";
3966
4022
  async function viewer(context) {
3967
4023
  const email = context.get("verifiedViewerEmail")?.trim();
3968
4024
  return email ? await resolveViewerUser(email) : void 0;
@@ -3977,6 +4033,14 @@ function createTaskRoutes() {
3977
4033
  { status: 401 }
3978
4034
  );
3979
4035
  });
4036
+ app.get("/runs", async (context) => {
4037
+ const user = await viewer(context);
4038
+ return user ? jsonResponse(taskRunListSchema, await readViewerTaskRuns(user)) : jsonResponse(
4039
+ apiErrorSchema,
4040
+ { error: "Authentication required." },
4041
+ { status: 401 }
4042
+ );
4043
+ });
3980
4044
  app.get("/:kind/:id/executions", async (context) => {
3981
4045
  const user = await viewer(context);
3982
4046
  if (!user) {
@@ -3997,11 +4061,7 @@ function createTaskRoutes() {
3997
4061
  try {
3998
4062
  return jsonResponse(
3999
4063
  taskExecutionListSchema,
4000
- await readViewerTaskExecutions(
4001
- user,
4002
- params.data.kind,
4003
- params.data.id
4004
- )
4064
+ await readViewerTaskExecutions(user, params.data.kind, params.data.id)
4005
4065
  );
4006
4066
  } catch (error) {
4007
4067
  if (error instanceof ViewerTaskNotFoundError) {