@oneuptime/common 11.5.8 → 11.5.9

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 (195) hide show
  1. package/Models/DatabaseModels/AIAgentTaskPullRequest.ts +15 -6
  2. package/Models/DatabaseModels/AIRun.ts +37 -0
  3. package/Models/DatabaseModels/AIRunEvent.ts +34 -1
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +215 -0
  5. package/Models/DatabaseModels/Project.ts +25 -0
  6. package/Models/DatabaseModels/StatusPage.ts +42 -0
  7. package/Server/API/AIAgentTaskAPI.ts +38 -12
  8. package/Server/API/AIAgentTaskLogAPI.ts +49 -0
  9. package/Server/API/AIChatAPI.ts +16 -0
  10. package/Server/API/AIReadinessAPI.ts +84 -0
  11. package/Server/API/CodeFixRunAPI.ts +182 -0
  12. package/Server/API/TelemetryExceptionAPI.ts +6 -1
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.ts +95 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.ts +52 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.ts +30 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.ts +49 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  18. package/Server/Services/AIRunEventService.ts +15 -1
  19. package/Server/Services/AIRunService.ts +38 -1
  20. package/Server/Services/LlmProviderService.ts +2 -0
  21. package/Server/Services/ProjectService.ts +42 -0
  22. package/Server/Services/StatusPageService.ts +87 -0
  23. package/Server/Services/TelemetryExceptionService.ts +19 -90
  24. package/Server/Types/Markdown.ts +9 -9
  25. package/Server/Types/MarkdownSlugify.ts +47 -0
  26. package/Server/Utils/AI/AIRunTranscript.ts +240 -0
  27. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +9 -0
  28. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +119 -5
  29. package/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.ts +75 -0
  30. package/Server/Utils/AI/CodeFix/CodeFixReadiness.ts +235 -0
  31. package/Server/Utils/AI/SRE/Insights/FixRouting.ts +2 -3
  32. package/Server/Utils/AI/Toolbox/CodeTools.ts +883 -0
  33. package/Server/Utils/AI/Toolbox/CodeWriteTools.ts +519 -0
  34. package/Server/Utils/AI/Toolbox/Index.ts +27 -0
  35. package/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.ts +279 -0
  36. package/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.ts +19 -1
  37. package/Server/Utils/CodeRepository/GitHub/GitHub.ts +600 -0
  38. package/Server/Utils/CodeRepository/StackTraceRepoResolver.ts +7 -2
  39. package/Server/Utils/LLM/LLMService.ts +397 -18
  40. package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +4 -11
  41. package/Tests/Models/StatusPageEnableMcpServer.test.ts +125 -0
  42. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +223 -0
  43. package/Tests/Server/Services/TelemetryExceptionAIFixReadiness.test.ts +26 -3
  44. package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +12 -0
  45. package/Tests/Server/Types/Markdown.test.ts +106 -0
  46. package/Tests/Server/Utils/AI/AIRunTranscript.test.ts +182 -0
  47. package/Tests/Server/Utils/AI/CodeFixReadiness.test.ts +411 -0
  48. package/Tests/Server/Utils/AI/CodeTools.test.ts +596 -0
  49. package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +475 -0
  50. package/Tests/Server/Utils/AI/LLMServiceModelCompatibility.test.ts +548 -0
  51. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +142 -0
  52. package/Tests/Server/Utils/AI/ScheduledMaintenanceTools.test.ts +176 -0
  53. package/Tests/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.test.ts +63 -0
  54. package/Tests/Server/Utils/GitHubGetFileContent.test.ts +197 -0
  55. package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +502 -0
  56. package/Tests/Types/AI/AIAgentTaskStatus.test.ts +7 -6
  57. package/Tests/Types/AI/AIChatPageContext.test.ts +0 -0
  58. package/Tests/Types/AI/AIRunStatus.test.ts +42 -0
  59. package/Tests/Types/Log/LogSeverity.test.ts +78 -0
  60. package/Tests/UI/Components/BasicFormDropdownNormalization.test.tsx +167 -0
  61. package/Tests/UI/Components/Charts/AxisTickColor.test.tsx +66 -0
  62. package/Tests/UI/Utils/AIChatExport/ChatWidgetData.test.ts +319 -0
  63. package/Tests/UI/Utils/AIChatExport/ChatWidgetMarkdown.test.ts +188 -0
  64. package/Types/AI/AIAgentTaskStatus.ts +13 -0
  65. package/Types/AI/AIChatPageContext.ts +137 -0
  66. package/Types/AI/AIChatTypes.ts +44 -0
  67. package/Types/AI/AIFixReadiness.ts +46 -0
  68. package/Types/AI/AIRunStatus.ts +11 -0
  69. package/Types/Log/LogSeverity.ts +61 -0
  70. package/Types/Monitor/SnmpMonitor/SnmpVersion.ts +40 -0
  71. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +8 -2
  72. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +3 -1
  73. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +8 -2
  74. package/UI/Components/Forms/BasicForm.tsx +26 -8
  75. package/UI/Styles/Theme.css +1314 -0
  76. package/UI/Utils/AIChatExport/ChatWidgetData.ts +316 -0
  77. package/UI/Utils/AIChatExport/ChatWidgetMarkdown.ts +349 -0
  78. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +177 -0
  79. package/UI/Utils/AIChatExport/MarkdownBlocks.ts +327 -0
  80. package/UI/Utils/AIChatExport/MarkdownSafety.ts +215 -0
  81. package/UI/Utils/DownloadFile.ts +54 -0
  82. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js +15 -7
  83. package/build/dist/Models/DatabaseModels/AIAgentTaskPullRequest.js.map +1 -1
  84. package/build/dist/Models/DatabaseModels/AIRun.js +38 -0
  85. package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
  86. package/build/dist/Models/DatabaseModels/AIRunEvent.js +31 -0
  87. package/build/dist/Models/DatabaseModels/AIRunEvent.js.map +1 -1
  88. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +225 -0
  89. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  90. package/build/dist/Models/DatabaseModels/Project.js +27 -0
  91. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  92. package/build/dist/Models/DatabaseModels/StatusPage.js +43 -0
  93. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  94. package/build/dist/Server/API/AIAgentTaskAPI.js +37 -11
  95. package/build/dist/Server/API/AIAgentTaskAPI.js.map +1 -1
  96. package/build/dist/Server/API/AIAgentTaskLogAPI.js +32 -7
  97. package/build/dist/Server/API/AIAgentTaskLogAPI.js.map +1 -1
  98. package/build/dist/Server/API/AIChatAPI.js +9 -0
  99. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  100. package/build/dist/Server/API/AIReadinessAPI.js +57 -0
  101. package/build/dist/Server/API/AIReadinessAPI.js.map +1 -0
  102. package/build/dist/Server/API/CodeFixRunAPI.js +142 -0
  103. package/build/dist/Server/API/CodeFixRunAPI.js.map +1 -1
  104. package/build/dist/Server/API/TelemetryExceptionAPI.js +4 -0
  105. package/build/dist/Server/API/TelemetryExceptionAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js +80 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784124919694-AddAITaskNumberAndRunTranscript.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js +37 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784135099754-AllowNullAiAgentOnPullRequest.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js +23 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784137457184-AddEnableMcpServerToStatusPage.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js +22 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784200000000-AddSnmpV3ColumnsToNetworkDeviceDiscoveryScan.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Services/AIRunEventService.js +8 -0
  117. package/build/dist/Server/Services/AIRunEventService.js.map +1 -1
  118. package/build/dist/Server/Services/AIRunService.js +29 -0
  119. package/build/dist/Server/Services/AIRunService.js.map +1 -1
  120. package/build/dist/Server/Services/LlmProviderService.js +2 -0
  121. package/build/dist/Server/Services/LlmProviderService.js.map +1 -1
  122. package/build/dist/Server/Services/ProjectService.js +39 -0
  123. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/StatusPageService.js +81 -0
  125. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  126. package/build/dist/Server/Services/TelemetryExceptionService.js +14 -61
  127. package/build/dist/Server/Services/TelemetryExceptionService.js.map +1 -1
  128. package/build/dist/Server/Types/Markdown.js +8 -8
  129. package/build/dist/Server/Types/Markdown.js.map +1 -1
  130. package/build/dist/Server/Types/MarkdownSlugify.js +47 -0
  131. package/build/dist/Server/Types/MarkdownSlugify.js.map +1 -0
  132. package/build/dist/Server/Utils/AI/AIRunTranscript.js +180 -0
  133. package/build/dist/Server/Utils/AI/AIRunTranscript.js.map +1 -0
  134. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +1 -0
  135. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  136. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +103 -5
  137. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  138. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js +55 -0
  139. package/build/dist/Server/Utils/AI/CodeFix/CodeFixAgentCompletion.js.map +1 -1
  140. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js +211 -0
  141. package/build/dist/Server/Utils/AI/CodeFix/CodeFixReadiness.js.map +1 -0
  142. package/build/dist/Server/Utils/AI/SRE/Insights/FixRouting.js.map +1 -1
  143. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +654 -0
  144. package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -0
  145. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js +389 -0
  146. package/build/dist/Server/Utils/AI/Toolbox/CodeWriteTools.js.map +1 -0
  147. package/build/dist/Server/Utils/AI/Toolbox/Index.js +19 -0
  148. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  149. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js +239 -0
  150. package/build/dist/Server/Utils/AI/Toolbox/ScheduledMaintenanceTools.js.map +1 -0
  151. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js +19 -1
  152. package/build/dist/Server/Utils/AnalyticsDatabase/ClickhouseCapacity.js.map +1 -1
  153. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +439 -0
  154. package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
  155. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js +1 -1
  156. package/build/dist/Server/Utils/CodeRepository/StackTraceRepoResolver.js.map +1 -1
  157. package/build/dist/Server/Utils/LLM/LLMService.js +275 -19
  158. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +2 -11
  160. package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
  161. package/build/dist/Types/AI/AIAgentTaskStatus.js +12 -0
  162. package/build/dist/Types/AI/AIAgentTaskStatus.js.map +1 -1
  163. package/build/dist/Types/AI/AIChatPageContext.js +101 -0
  164. package/build/dist/Types/AI/AIChatPageContext.js.map +1 -0
  165. package/build/dist/Types/AI/AIChatTypes.js +2 -0
  166. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  167. package/build/dist/Types/AI/AIFixReadiness.js +9 -0
  168. package/build/dist/Types/AI/AIFixReadiness.js.map +1 -0
  169. package/build/dist/Types/AI/AIRunStatus.js +11 -0
  170. package/build/dist/Types/AI/AIRunStatus.js.map +1 -1
  171. package/build/dist/Types/Log/LogSeverity.js +56 -0
  172. package/build/dist/Types/Log/LogSeverity.js.map +1 -1
  173. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js +38 -0
  174. package/build/dist/Types/Monitor/SnmpMonitor/SnmpVersion.js.map +1 -1
  175. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +9 -2
  176. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  177. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +3 -1
  178. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  179. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +9 -2
  180. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/BasicForm.js +24 -6
  182. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  183. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js +217 -0
  184. package/build/dist/UI/Utils/AIChatExport/ChatWidgetData.js.map +1 -0
  185. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js +234 -0
  186. package/build/dist/UI/Utils/AIChatExport/ChatWidgetMarkdown.js.map +1 -0
  187. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +111 -0
  188. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -0
  189. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js +233 -0
  190. package/build/dist/UI/Utils/AIChatExport/MarkdownBlocks.js.map +1 -0
  191. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js +134 -0
  192. package/build/dist/UI/Utils/AIChatExport/MarkdownSafety.js.map +1 -0
  193. package/build/dist/UI/Utils/DownloadFile.js +41 -0
  194. package/build/dist/UI/Utils/DownloadFile.js.map +1 -0
  195. package/package.json +5 -1
@@ -0,0 +1,654 @@
1
+ import CodeRepository from "../../../../Models/DatabaseModels/CodeRepository";
2
+ import TelemetryException from "../../../../Models/DatabaseModels/TelemetryException";
3
+ import CodeRepositoryType from "../../../../Types/CodeRepository/CodeRepositoryType";
4
+ import BadDataException from "../../../../Types/Exception/BadDataException";
5
+ import ObjectID from "../../../../Types/ObjectID";
6
+ import LIMIT_MAX from "../../../../Types/Database/LimitMax";
7
+ import { AIChatCitationTargetType } from "../../../../Types/AI/AIChatTypes";
8
+ import CodeRepositoryService from "../../../Services/CodeRepositoryService";
9
+ import TelemetryExceptionService from "../../../Services/TelemetryExceptionService";
10
+ import GitHubUtil from "../../CodeRepository/GitHub/GitHub";
11
+ import { normalizeCandidatePath, } from "../../CodeRepository/StackTraceRepoResolver";
12
+ import logger from "../../Logger";
13
+ import StackTraceParser from "../../Telemetry/StackTraceParser";
14
+ import ToolResultSerializer from "./Serializer";
15
+ import { ToolArgs, } from "./ToolTypes";
16
+ /*
17
+ * Tools that let the chat agent read the project's linked source code and
18
+ * connect a telemetry signal to the code that produced it.
19
+ *
20
+ * The trust posture here is deliberately narrower than the observability
21
+ * tools': source is read-only, GitHub-App-connected repositories only, and
22
+ * every payload goes through the same secret redaction as telemetry (a repo's
23
+ * config files are a likelier place to find a live credential than a log line
24
+ * is).
25
+ */
26
+ /*
27
+ * Derived from the model ACL so the tool gate can never drift from RBAC.
28
+ * Resolved lazily, not at module load: this module is pulled in through the
29
+ * service import graph before the model classes are fully wired up, so calling
30
+ * a model method at import time throws a circular-dependency TypeError. By the
31
+ * time a tool actually executes, every module is loaded. (Same reasoning as
32
+ * ExceptionTools.)
33
+ */
34
+ let cachedRepositoryReadPermissions = null;
35
+ const resolveRepositoryReadPermissions = () => {
36
+ if (!cachedRepositoryReadPermissions) {
37
+ cachedRepositoryReadPermissions =
38
+ new CodeRepository().getReadPermissions();
39
+ }
40
+ return cachedRepositoryReadPermissions;
41
+ };
42
+ /*
43
+ * find_code_for_exception touches both an exception and a repository, so it
44
+ * offers both permission sets here. Note the toolbox gate is an INTERSECTION
45
+ * test (PermissionHelper.doesPermissionsIntersect) — holding either set opens
46
+ * the tool, not both. That is deliberate and safe: the real enforcement is at
47
+ * the query layer, where the exception is fetched with ctx.props (so a
48
+ * repo-only user gets "not found") and the repository is re-checked through
49
+ * findReadableRepositories. This list is defense-in-depth, not the gate.
50
+ */
51
+ let cachedExceptionReadPermissions = null;
52
+ const resolveExceptionReadPermissions = () => {
53
+ if (!cachedExceptionReadPermissions) {
54
+ cachedExceptionReadPermissions =
55
+ new TelemetryException().getReadPermissions();
56
+ }
57
+ return cachedExceptionReadPermissions;
58
+ };
59
+ /*
60
+ * Line budget for one read_code_file call. The chat loop allows 16 tool calls
61
+ * against a context window nothing in this codebase measures (there is no
62
+ * tokenizer and no per-model context length anywhere), so the cap is a
63
+ * deliberately conservative guess rather than a computed fit: ~400 lines is
64
+ * roughly 4-6k tokens, leaving room for several reads plus the telemetry that
65
+ * motivated them.
66
+ */
67
+ const MAX_LINES_PER_READ = 400;
68
+ const DEFAULT_CONTEXT_LINES = 60;
69
+ const MAX_SEARCH_RESULTS = 40;
70
+ /*
71
+ * Byte budget for the code block, deliberately under ToolResultSerializer's own
72
+ * 16KB payload cap. Staying below it means the serializer's blind hard-slice
73
+ * never fires on code, so the range the header reports is always the range the
74
+ * payload actually contains. The headroom absorbs redactions that grow the text
75
+ * (e.g. "1.2.3.4" → "[redacted-ip]").
76
+ */
77
+ const MAX_CODE_PAYLOAD_BYTES = 12 * 1024;
78
+ /*
79
+ * The repositories this user can see, already filtered to the ones that are
80
+ * actually readable. A repository row can exist without a usable GitHub App
81
+ * installation (e.g. imported then uninstalled, or a GitLab row — GitLab has
82
+ * no read path yet), and those must never look readable to the model.
83
+ */
84
+ export async function findReadableRepositories(ctx, codeRepositoryId) {
85
+ const query = {
86
+ projectId: ctx.projectId,
87
+ };
88
+ if (codeRepositoryId) {
89
+ query["_id"] = codeRepositoryId.toString();
90
+ }
91
+ const repositories = await CodeRepositoryService.findBy({
92
+ query: query,
93
+ select: {
94
+ _id: true,
95
+ name: true,
96
+ organizationName: true,
97
+ repositoryName: true,
98
+ mainBranchName: true,
99
+ gitHubAppInstallationId: true,
100
+ repositoryHostedAt: true,
101
+ description: true,
102
+ },
103
+ limit: LIMIT_MAX,
104
+ skip: 0,
105
+ props: ctx.props,
106
+ });
107
+ return repositories.filter((repository) => {
108
+ return (repository.repositoryHostedAt === CodeRepositoryType.GitHub &&
109
+ Boolean(repository.gitHubAppInstallationId));
110
+ });
111
+ }
112
+ /*
113
+ * Resolve the one repository a tool call should act on. Ambiguity is an error
114
+ * the model can fix (by naming a repositoryId) rather than something to guess
115
+ * at — guessing would silently answer a question about the wrong codebase.
116
+ */
117
+ export async function resolveTargetRepository(ctx, args) {
118
+ const requestedId = ToolArgs.getObjectID(args, "repositoryId");
119
+ const repositories = await findReadableRepositories(ctx, requestedId);
120
+ if (repositories.length === 0) {
121
+ if (requestedId) {
122
+ throw new BadDataException(`No readable repository with id ${requestedId.toString()} in this project. Call list_code_repositories to see which repositories are connected.`);
123
+ }
124
+ throw new BadDataException("This project has no GitHub-connected code repositories, so source code cannot be read. Connect a repository under Code Repository in the dashboard.");
125
+ }
126
+ if (repositories.length > 1) {
127
+ const names = repositories
128
+ .map((repository) => {
129
+ var _a;
130
+ return `${repository.name} (repositoryId=${(_a = repository.id) === null || _a === void 0 ? void 0 : _a.toString()})`;
131
+ })
132
+ .join(", ");
133
+ throw new BadDataException(`This project has several repositories, so repositoryId is required. Choose one of: ${names}.`);
134
+ }
135
+ return repositories[0];
136
+ }
137
+ /*
138
+ * The exception's identity, redacted and length-capped. exception.message is
139
+ * populated verbatim from the monitored application's thrown error, so it is
140
+ * both the likeliest place for a stray credential and unbounded in length
141
+ * (a VeryLongText column). Every other tool that surfaces it routes it through
142
+ * the serializer; this keeps that invariant instead of interpolating it raw.
143
+ *
144
+ * The redaction count is returned, not discarded: it feeds the AIRun egress
145
+ * manifest, so dropping it would under-report what left the tenant.
146
+ */
147
+ function summarizeExceptionForLlm(exception) {
148
+ const serialized = ToolResultSerializer.serializeRows([
149
+ {
150
+ exceptionType: exception.exceptionType || "Error",
151
+ message: exception.message,
152
+ occurrences: exception.occuranceCount,
153
+ },
154
+ ]);
155
+ return {
156
+ text: `exception: ${serialized.text.replace(/^- /, "")}`,
157
+ redactionCount: serialized.redactionCount,
158
+ };
159
+ }
160
+ /*
161
+ * The repository's file tree, for turning runtime stack-trace paths into real
162
+ * repository paths. The resolver has already fetched (and cached for an hour)
163
+ * this exact tree while matching the stack trace, so this is a cache hit in
164
+ * the normal path rather than a second API round-trip.
165
+ */
166
+ async function getTreePathsForResolution(ctx, resolution) {
167
+ const repositories = await findReadableRepositories(ctx, new ObjectID(resolution.codeRepositoryId));
168
+ const repository = repositories[0];
169
+ if (!repository) {
170
+ return [];
171
+ }
172
+ try {
173
+ return await GitHubUtil.getRepositoryTreePaths({
174
+ installationId: repository.gitHubAppInstallationId,
175
+ organizationName: repository.organizationName,
176
+ repositoryName: repository.repositoryName,
177
+ branchName: repository.mainBranchName || "main",
178
+ });
179
+ }
180
+ catch (err) {
181
+ /*
182
+ * A tree fetch failure must not sink the whole answer: the frames are still
183
+ * worth reporting, just without openable paths.
184
+ */
185
+ logger.debug(err);
186
+ return [];
187
+ }
188
+ }
189
+ /*
190
+ * Map one stack frame's runtime path onto a real path in the repository tree.
191
+ * Returns null unless the match is unambiguous — handing the model a
192
+ * confidently-wrong path is worse than telling it the path is unknown, because
193
+ * it would then read and reason about the wrong file.
194
+ */
195
+ function matchFrameToRepositoryPath(rawFramePath, treePaths) {
196
+ if (treePaths.length === 0) {
197
+ return null;
198
+ }
199
+ // Strips container prefixes (/app, /usr/src/app) and rejects dependency noise.
200
+ const normalized = normalizeCandidatePath(rawFramePath);
201
+ if (!normalized) {
202
+ return null;
203
+ }
204
+ // An exact tree path wins outright.
205
+ if (treePaths.includes(normalized)) {
206
+ return normalized;
207
+ }
208
+ /*
209
+ * Otherwise the frame path is a suffix of the repository path (the runtime
210
+ * working directory swallowed the leading segments). Longest suffix wins;
211
+ * ties mean genuinely ambiguous and resolve to null.
212
+ */
213
+ const suffixMatches = treePaths.filter((treePath) => {
214
+ return treePath.endsWith(`/${normalized}`);
215
+ });
216
+ if (suffixMatches.length === 1) {
217
+ return suffixMatches[0];
218
+ }
219
+ return null;
220
+ }
221
+ /*
222
+ * Split file content into lines without inventing a phantom blank line for the
223
+ * trailing newline that virtually every source file ends with.
224
+ */
225
+ function splitIntoLines(content) {
226
+ if (content === "") {
227
+ return [];
228
+ }
229
+ const lines = content.split("\n");
230
+ if (lines[lines.length - 1] === "") {
231
+ lines.pop();
232
+ }
233
+ return lines;
234
+ }
235
+ /*
236
+ * Take as many whole lines as fit the code payload budget. Whole lines, not
237
+ * bytes: a mid-line cut would hand the model a truncated identifier that reads
238
+ * like real code.
239
+ */
240
+ function takeLinesWithinByteBudget(lines, startLine) {
241
+ const kept = [];
242
+ // Each line also carries a "NNN| " prefix once numbered.
243
+ const prefixWidth = String(startLine + lines.length - 1).length + 2;
244
+ let bytes = 0;
245
+ for (const line of lines) {
246
+ const lineBytes = Buffer.byteLength(line, "utf8") + prefixWidth + 1;
247
+ if (kept.length > 0 && bytes + lineBytes > MAX_CODE_PAYLOAD_BYTES) {
248
+ break;
249
+ }
250
+ bytes += lineBytes;
251
+ kept.push(line);
252
+ }
253
+ return kept;
254
+ }
255
+ // Prefix each line with its real file line number so the model can cite lines.
256
+ function formatWithLineNumbers(data) {
257
+ const width = String(data.startLine + data.lines.length - 1).length;
258
+ return data.lines
259
+ .map((line, index) => {
260
+ const lineNumber = String(data.startLine + index).padStart(width, " ");
261
+ return `${lineNumber}| ${line}`;
262
+ })
263
+ .join("\n");
264
+ }
265
+ export const ListCodeRepositoriesTool = {
266
+ name: "list_code_repositories",
267
+ description: "List the code repositories connected to this project, with the repositoryId needed by search_code and read_code_file. Call this first when the user asks about source code and you do not already know which repository to look in.",
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {},
271
+ },
272
+ get requiredPermissions() {
273
+ return resolveRepositoryReadPermissions();
274
+ },
275
+ execute: async (_args, ctx) => {
276
+ const repositories = await findReadableRepositories(ctx);
277
+ const rows = repositories.map((repository) => {
278
+ var _a;
279
+ return {
280
+ repositoryId: (_a = repository.id) === null || _a === void 0 ? void 0 : _a.toString(),
281
+ name: repository.name,
282
+ repository: `${repository.organizationName}/${repository.repositoryName}`,
283
+ branch: repository.mainBranchName,
284
+ description: repository.description,
285
+ };
286
+ });
287
+ const serialized = ToolResultSerializer.serializeRows(rows);
288
+ return {
289
+ dataForLlm: rows.length === 0
290
+ ? "(no GitHub-connected code repositories in this project — source code cannot be read)"
291
+ : serialized.text,
292
+ rowCount: serialized.rowCount,
293
+ citationLabel: `Connected code repositories (${serialized.rowCount})`,
294
+ redactionCount: serialized.redactionCount,
295
+ isTruncated: serialized.isTruncated,
296
+ };
297
+ },
298
+ };
299
+ export const FindCodeForExceptionTool = {
300
+ name: "find_code_for_exception",
301
+ description: "Given an exception id (from top_exceptions), work out which connected repository its code lives in and which source files and line numbers its stack trace implicates. This is the bridge from a telemetry signal to the code that caused it: call it before read_code_file when investigating an exception's root cause.",
302
+ inputSchema: {
303
+ type: "object",
304
+ properties: {
305
+ exceptionId: {
306
+ type: "string",
307
+ description: "The exception id returned by top_exceptions (the `id` field).",
308
+ },
309
+ },
310
+ required: ["exceptionId"],
311
+ },
312
+ get requiredPermissions() {
313
+ return [
314
+ ...resolveExceptionReadPermissions(),
315
+ ...resolveRepositoryReadPermissions(),
316
+ ];
317
+ },
318
+ execute: async (args, ctx) => {
319
+ const exceptionId = ToolArgs.getObjectID(args, "exceptionId");
320
+ if (!exceptionId) {
321
+ throw new BadDataException("exceptionId is required. Get one from top_exceptions.");
322
+ }
323
+ // props (not isRoot) so an unauthorized exception reads as "not found".
324
+ const exception = await TelemetryExceptionService.findOneById({
325
+ id: exceptionId,
326
+ select: {
327
+ _id: true,
328
+ message: true,
329
+ exceptionType: true,
330
+ stackTrace: true,
331
+ fingerprint: true,
332
+ occuranceCount: true,
333
+ firstSeenAt: true,
334
+ lastSeenAt: true,
335
+ },
336
+ props: ctx.props,
337
+ });
338
+ if (!exception) {
339
+ throw new BadDataException(`No exception found with id ${exceptionId.toString()} in this project.`);
340
+ }
341
+ const stackTrace = exception.stackTrace || "";
342
+ /*
343
+ * The exception's own message is the most attacker-influenceable string in
344
+ * the whole tool belt — it is whatever the monitored app threw — so it gets
345
+ * the same redaction and length cap every other tool applies to it via the
346
+ * serializer, rather than being interpolated raw into the header.
347
+ */
348
+ const described = summarizeExceptionForLlm(exception);
349
+ const describedException = described.text;
350
+ if (!stackTrace.trim()) {
351
+ return {
352
+ dataForLlm: `${describedException}\n\nThis exception has no stack trace recorded, so its code location cannot be determined. Consider search_code if you know roughly what the code is called.`,
353
+ rowCount: 0,
354
+ citationLabel: `Code location for exception (no stack trace)`,
355
+ redactionCount: described.redactionCount,
356
+ isTruncated: false,
357
+ };
358
+ }
359
+ const resolution = await CodeRepositoryService.resolveRepositoryForException({
360
+ projectId: ctx.projectId,
361
+ stackTrace: stackTrace,
362
+ serviceName: null,
363
+ });
364
+ /*
365
+ * resolveRepositoryForException queries as isRoot (it serves the autonomous
366
+ * fix agent, which has no user), so it can name a repository this user is
367
+ * not allowed to see — CodeRepository is label-access-controlled. Re-check
368
+ * the answer against the user's own readable set before disclosing it.
369
+ */
370
+ let readableResolution = null;
371
+ if (resolution) {
372
+ const readable = await findReadableRepositories(ctx, new ObjectID(resolution.codeRepositoryId));
373
+ if (readable.length > 0) {
374
+ readableResolution = resolution;
375
+ }
376
+ }
377
+ const parsed = StackTraceParser.parse(stackTrace);
378
+ /*
379
+ * in-app frames first: library frames are almost never the fix site, and
380
+ * the model will otherwise happily read node_modules. Order is preserved
381
+ * within each group so the throw site stays at the top.
382
+ */
383
+ const appFrames = parsed.frames.filter((frame) => {
384
+ return frame.inApp;
385
+ });
386
+ const selectedFrames = (appFrames.length > 0 ? appFrames : parsed.frames).slice(0, 15);
387
+ /*
388
+ * A frame's fileName is a RUNTIME path (/app/src/billing/charge.ts); the
389
+ * Contents API needs a REPOSITORY path (src/billing/charge.ts). Without
390
+ * this mapping every path handed to read_code_file 404s, which is the whole
391
+ * point of the tool. Match each frame against the repository's real tree
392
+ * (already cached for an hour by the resolver's own probe) so the model is
393
+ * only ever given paths that open.
394
+ */
395
+ const treePaths = readableResolution
396
+ ? await getTreePathsForResolution(ctx, readableResolution)
397
+ : [];
398
+ const frameRows = selectedFrames.map((frame) => {
399
+ const repoPath = matchFrameToRepositoryPath(frame.fileName, treePaths);
400
+ return {
401
+ file: repoPath || frame.fileName,
402
+ line: frame.lineNumber,
403
+ function: frame.functionName,
404
+ isApplicationCode: frame.inApp,
405
+ // Only a matched path is safe to hand to read_code_file.
406
+ openableWithReadCodeFile: Boolean(repoPath),
407
+ };
408
+ });
409
+ const serialized = ToolResultSerializer.serializeRows(frameRows);
410
+ const header = [describedException];
411
+ if (readableResolution) {
412
+ const openable = frameRows.filter((row) => {
413
+ return row["openableWithReadCodeFile"] === true;
414
+ }).length;
415
+ header.push(`resolvedRepository: ${readableResolution.organizationName}/${readableResolution.repositoryName}`, `repositoryId: ${readableResolution.codeRepositoryId}`, `howResolved: ${readableResolution.method} (${readableResolution.evidence})`);
416
+ if (openable > 0) {
417
+ header.push(`Use read_code_file with repositoryId=${readableResolution.codeRepositoryId} and a file path below that has openableWithReadCodeFile=true, passing aroundLine=<the frame's line>.`);
418
+ }
419
+ else {
420
+ header.push(`None of these frames could be matched to a file in the repository's tree, so their paths are raw runtime paths and read_code_file will not open them. Use search_code to find the real path first.`);
421
+ }
422
+ }
423
+ else {
424
+ header.push(`resolvedRepository: none — could not match this stack trace to any connected repository you can read. Say so rather than guessing which repo or file it is.`);
425
+ }
426
+ if (appFrames.length === 0 && parsed.frames.length > 0) {
427
+ header.push(`note: every frame looks like library/framework code, so these are NOT application code.`);
428
+ }
429
+ const framesText = frameRows.length > 0
430
+ ? `\n\nstack frames (most recent first):\n${serialized.text}`
431
+ : `\n\nThe stack trace could not be parsed into frames. Raw stack trace:\n${ToolResultSerializer.serializeText(stackTrace, 1).text}`;
432
+ return {
433
+ dataForLlm: `${header.join("\n")}${framesText}`,
434
+ rowCount: frameRows.length,
435
+ citationLabel: readableResolution
436
+ ? `Code location: ${readableResolution.organizationName}/${readableResolution.repositoryName} (${frameRows.length} frames)`
437
+ : `Code location for exception (no repository matched)`,
438
+ citationTarget: {
439
+ type: AIChatCitationTargetType.Exceptions,
440
+ },
441
+ // Both payloads egress, so the manifest must count both.
442
+ redactionCount: described.redactionCount + serialized.redactionCount,
443
+ isTruncated: serialized.isTruncated,
444
+ };
445
+ },
446
+ };
447
+ export const SearchCodeTool = {
448
+ name: "search_code",
449
+ description: "Find source file paths in a connected repository by matching part of a file or directory name (e.g. 'checkout', 'billing/charge.ts'). Returns paths only, not contents — follow up with read_code_file. Use this to locate code when you do not have a stack trace.",
450
+ inputSchema: {
451
+ type: "object",
452
+ properties: {
453
+ query: {
454
+ type: "string",
455
+ description: "Part of a file path or file name to match, case-insensitive (e.g. 'PaymentService' or 'src/billing').",
456
+ },
457
+ repositoryId: {
458
+ type: "string",
459
+ description: "Which repository to search, from list_code_repositories. Optional when the project has exactly one connected repository.",
460
+ },
461
+ limit: {
462
+ type: "number",
463
+ description: `Maximum paths to return (default 20, max ${MAX_SEARCH_RESULTS}).`,
464
+ },
465
+ },
466
+ required: ["query"],
467
+ },
468
+ get requiredPermissions() {
469
+ return resolveRepositoryReadPermissions();
470
+ },
471
+ execute: async (args, ctx) => {
472
+ const query = ToolArgs.getString(args, "query");
473
+ if (!query) {
474
+ throw new BadDataException("query is required — pass part of a file or directory name to match.");
475
+ }
476
+ const limit = ToolArgs.getNumber(args, "limit", {
477
+ defaultValue: 20,
478
+ min: 1,
479
+ max: MAX_SEARCH_RESULTS,
480
+ });
481
+ const repository = await resolveTargetRepository(ctx, args);
482
+ const paths = await GitHubUtil.getRepositoryTreePaths({
483
+ installationId: repository.gitHubAppInstallationId,
484
+ organizationName: repository.organizationName,
485
+ repositoryName: repository.repositoryName,
486
+ branchName: repository.mainBranchName || "main",
487
+ });
488
+ const needle = query.toLowerCase();
489
+ const matches = paths.filter((path) => {
490
+ return path.toLowerCase().includes(needle);
491
+ });
492
+ /*
493
+ * Shortest-first: 'src/billing/charge.ts' is a likelier intent than
494
+ * 'src/billing/__tests__/charge.fixture.ts', and the cap would otherwise
495
+ * cut off the file the user actually meant.
496
+ */
497
+ matches.sort((a, b) => {
498
+ return a.length - b.length;
499
+ });
500
+ const limited = matches.slice(0, limit);
501
+ const text = limited.length === 0
502
+ ? `(no file paths in ${repository.organizationName}/${repository.repositoryName} match "${query}")`
503
+ : limited
504
+ .map((path) => {
505
+ return `- ${path}`;
506
+ })
507
+ .join("\n");
508
+ const isTruncated = matches.length > limited.length;
509
+ return {
510
+ dataForLlm: isTruncated
511
+ ? `${text}\n… [showing ${limited.length} of ${matches.length} matches; narrow the query to see the rest]`
512
+ : text,
513
+ rowCount: limited.length,
514
+ citationLabel: `Code search "${query}" in ${repository.organizationName}/${repository.repositoryName} (${limited.length} of ${matches.length})`,
515
+ redactionCount: 0,
516
+ isTruncated: isTruncated,
517
+ };
518
+ },
519
+ };
520
+ export const ReadCodeFileTool = {
521
+ name: "read_code_file",
522
+ description: "Read the source of one file from a connected repository, with line numbers. Pass startLine/endLine to read the region around a stack-trace line rather than the whole file. Quote only the few lines that matter in your answer.",
523
+ inputSchema: {
524
+ type: "object",
525
+ properties: {
526
+ filePath: {
527
+ type: "string",
528
+ description: "Repository-relative path, e.g. 'src/billing/charge.ts'. Use the exact path from find_code_for_exception or search_code.",
529
+ },
530
+ repositoryId: {
531
+ type: "string",
532
+ description: "Which repository to read from, from list_code_repositories or find_code_for_exception. Optional when the project has exactly one connected repository.",
533
+ },
534
+ startLine: {
535
+ type: "number",
536
+ description: "First line to read (1-based). Omit to read from the top of the file.",
537
+ },
538
+ endLine: {
539
+ type: "number",
540
+ description: `Last line to read (1-based, inclusive). At most ${MAX_LINES_PER_READ} lines are returned per call.`,
541
+ },
542
+ aroundLine: {
543
+ type: "number",
544
+ description: `Read a window centred on this line — the convenient way to inspect a stack-trace frame. Returns ${DEFAULT_CONTEXT_LINES} lines either side. Ignored when startLine is given.`,
545
+ },
546
+ },
547
+ required: ["filePath"],
548
+ },
549
+ get requiredPermissions() {
550
+ return resolveRepositoryReadPermissions();
551
+ },
552
+ execute: async (args, ctx) => {
553
+ const filePath = ToolArgs.getString(args, "filePath");
554
+ if (!filePath) {
555
+ throw new BadDataException("filePath is required.");
556
+ }
557
+ const repository = await resolveTargetRepository(ctx, args);
558
+ const branchName = repository.mainBranchName || "main";
559
+ const file = await GitHubUtil.getFileContent({
560
+ installationId: repository.gitHubAppInstallationId,
561
+ organizationName: repository.organizationName,
562
+ repositoryName: repository.repositoryName,
563
+ branchName: branchName,
564
+ filePath: filePath,
565
+ });
566
+ if (!file) {
567
+ throw new BadDataException(`No readable file at "${filePath}" on branch ${branchName} of ${repository.organizationName}/${repository.repositoryName}. It may not exist, or may be a directory or a binary file. Use search_code to find the exact path.`);
568
+ }
569
+ const allLines = splitIntoLines(file.content);
570
+ if (allLines.length === 0) {
571
+ return {
572
+ dataForLlm: `file: ${filePath}\nrepository: ${repository.organizationName}/${repository.repositoryName}@${branchName}\n\n(this file is empty — 0 bytes)`,
573
+ rowCount: 0,
574
+ citationLabel: `${filePath} (empty)`,
575
+ redactionCount: 0,
576
+ isTruncated: false,
577
+ };
578
+ }
579
+ /*
580
+ * Resolve the window. `aroundLine` is sugar for the overwhelmingly common
581
+ * case — "show me the code at the frame that threw" — so the model does
582
+ * not have to do off-by-one arithmetic on every stack frame.
583
+ */
584
+ let startLine = ToolArgs.getNumber(args, "startLine", {
585
+ defaultValue: 0,
586
+ min: 0,
587
+ max: allLines.length,
588
+ });
589
+ let endLine = ToolArgs.getNumber(args, "endLine", {
590
+ defaultValue: 0,
591
+ min: 0,
592
+ max: allLines.length,
593
+ });
594
+ const aroundLine = ToolArgs.getNumber(args, "aroundLine", {
595
+ defaultValue: 0,
596
+ min: 0,
597
+ max: allLines.length,
598
+ });
599
+ if (!startLine && aroundLine) {
600
+ startLine = Math.max(1, aroundLine - DEFAULT_CONTEXT_LINES);
601
+ // Never silently discard an endLine the caller actually asked for.
602
+ if (!endLine) {
603
+ endLine = Math.min(allLines.length, aroundLine + DEFAULT_CONTEXT_LINES);
604
+ }
605
+ }
606
+ if (!startLine) {
607
+ startLine = 1;
608
+ }
609
+ if (!endLine || endLine < startLine) {
610
+ endLine = allLines.length;
611
+ }
612
+ // Clamp the window rather than refusing: a partial read still answers.
613
+ const requestedEndLine = endLine;
614
+ if (endLine - startLine + 1 > MAX_LINES_PER_READ) {
615
+ endLine = startLine + MAX_LINES_PER_READ - 1;
616
+ }
617
+ /*
618
+ * Trim to a byte budget BEFORE serializing. serializeText hard-slices at
619
+ * its own payload cap, which would silently drop the tail mid-line while
620
+ * the header still advertised the full range — and the "read on from a
621
+ * later startLine" hint would then skip exactly the lines that were cut.
622
+ * Trimming by whole lines here keeps the reported range true.
623
+ */
624
+ const selected = takeLinesWithinByteBudget(allLines.slice(startLine - 1, endLine), startLine);
625
+ const actualEndLine = startLine + selected.length - 1;
626
+ const isTruncated = actualEndLine < requestedEndLine;
627
+ const numbered = formatWithLineNumbers({
628
+ lines: selected,
629
+ startLine: startLine,
630
+ });
631
+ /*
632
+ * Redaction matters more here than for telemetry: a repository's config
633
+ * and fixture files are a likelier home for a live credential than a log
634
+ * line is, and this payload egresses to the LLM provider.
635
+ */
636
+ const serialized = ToolResultSerializer.serializeText(numbered, selected.length);
637
+ const header = [
638
+ `file: ${filePath}`,
639
+ `repository: ${repository.organizationName}/${repository.repositoryName}@${branchName}`,
640
+ `showing lines ${startLine}-${actualEndLine} of ${file.totalLines}`,
641
+ ].join("\n");
642
+ const footer = isTruncated
643
+ ? `\n… [stopped at line ${actualEndLine}; call again with startLine=${actualEndLine + 1} to read on]`
644
+ : "";
645
+ return {
646
+ dataForLlm: `${header}\n\n${serialized.text}${footer}`,
647
+ rowCount: selected.length,
648
+ citationLabel: `${filePath}:${startLine}-${actualEndLine} (${repository.organizationName}/${repository.repositoryName})`,
649
+ redactionCount: serialized.redactionCount,
650
+ isTruncated: isTruncated || serialized.isTruncated,
651
+ };
652
+ },
653
+ };
654
+ //# sourceMappingURL=CodeTools.js.map