infraweaver 0.3.7 → 0.3.8

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 (175) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/claude.d.ts +1 -1
  3. package/dist/agents/claudePretoolGate.d.ts +0 -36
  4. package/dist/agents/toolPlaneWatch.d.ts +31 -0
  5. package/dist/cli.mjs +31903 -18736
  6. package/dist/external.d.ts +2 -1
  7. package/dist/index.js +31987 -18749
  8. package/dist/internal.js +132 -52
  9. package/dist/mcp/baseBranch.d.ts +41 -0
  10. package/dist/mcp/changeSummary.d.ts +1 -1
  11. package/dist/mcp/findingAudit.d.ts +2 -0
  12. package/dist/mcp/guardrails.d.ts +38 -0
  13. package/dist/mcp/localContext.d.ts +1 -1
  14. package/dist/mcp/moduleExtraction.d.ts +5 -0
  15. package/dist/mcp/pr.d.ts +0 -31
  16. package/dist/mcp/reproveSuggestions.d.ts +27 -0
  17. package/dist/mcp/review.d.ts +126 -2
  18. package/dist/mcp/reviewCommentLimit.d.ts +17 -0
  19. package/dist/mcp/reviewComments.d.ts +9 -5
  20. package/dist/mcp/reviewDedup.d.ts +1 -0
  21. package/dist/mcp/reviewMarkers.d.ts +10 -0
  22. package/dist/mcp/reviewProvenance.d.ts +20 -6
  23. package/dist/mcp/shared.d.ts +15 -0
  24. package/dist/mcp/suggestionProver.d.ts +3 -2
  25. package/dist/mcp/terraform/checkovSeverities.d.ts +7 -0
  26. package/dist/mcp/terraform/consolidation.d.ts +26 -0
  27. package/dist/mcp/terraform/cost.d.ts +2 -2
  28. package/dist/mcp/terraform/currency.d.ts +11 -0
  29. package/dist/mcp/terraform/deltaSummary.d.ts +11 -1
  30. package/dist/mcp/terraform/emittedArtefacts.d.ts +11 -0
  31. package/dist/mcp/terraform/evidence.d.ts +10 -0
  32. package/dist/mcp/terraform/gateScan.d.ts +2 -16
  33. package/dist/mcp/terraform/hcl.d.ts +15 -0
  34. package/dist/mcp/terraform/initArtefacts.d.ts +21 -0
  35. package/dist/mcp/terraform/localModules.d.ts +29 -0
  36. package/dist/mcp/terraform/moduleFindings.d.ts +38 -0
  37. package/dist/mcp/terraform/refactor/attribution.d.ts +1 -1
  38. package/dist/mcp/terraform/refactor/equivalence.d.ts +7 -0
  39. package/dist/mcp/terraform/refactor/expressions.d.ts +62 -0
  40. package/dist/mcp/terraform/refactor/references.d.ts +15 -0
  41. package/dist/mcp/terraform/refactor/resources.d.ts +11 -0
  42. package/dist/mcp/terraform/refactor/substitution.d.ts +55 -37
  43. package/dist/mcp/terraform/scanDelta.d.ts +28 -0
  44. package/dist/mcp/terraform/scanSession.d.ts +13 -0
  45. package/dist/mcp/terraform/scanners.d.ts +67 -18
  46. package/dist/mcp/terraform/secretScan.d.ts +19 -0
  47. package/dist/mcp/terraform/standardsReport.d.ts +1 -1
  48. package/dist/mcp/terraform/tools/plan.d.ts +1 -1
  49. package/dist/mcp/terraform/tools/scan.d.ts +7 -7
  50. package/dist/mcp/terraform/tools/validate.d.ts +7 -2
  51. package/dist/mcp/terraform/tools/verifyRemediation.d.ts +1 -1
  52. package/dist/mcp/terraform/treeLayout.d.ts +57 -0
  53. package/dist/mcp/terraform/types.d.ts +1 -1
  54. package/dist/phases/applyPlan.d.ts +0 -2
  55. package/dist/toolState.d.ts +3 -0
  56. package/dist/utils/agent/codexOAuth.d.ts +1 -1
  57. package/dist/utils/cli.d.ts +1 -7
  58. package/dist/utils/cloud/cloudReport.d.ts +1 -1
  59. package/dist/utils/config/baseRefConfig.d.ts +1 -1
  60. package/dist/utils/config/infraweaverConfig.d.ts +1 -1
  61. package/dist/utils/config/payload.d.ts +5 -0
  62. package/dist/utils/markdownTable.d.ts +6 -0
  63. package/dist/utils/setup/toolLicensing.d.ts +1 -1
  64. package/dist/utils/unifiedDiff.d.ts +69 -0
  65. package/package.json +34 -28
  66. package/src/agents/claude.ts +48 -13
  67. package/src/agents/claudePretoolGate.ts +9 -17
  68. package/src/agents/sessionLabeler.ts +3 -3
  69. package/src/agents/toolPlaneWatch.ts +71 -0
  70. package/src/external.ts +3 -1
  71. package/src/mcp/assess.ts +8 -8
  72. package/src/mcp/baseBranch.ts +96 -0
  73. package/src/mcp/changeSummary.ts +10 -11
  74. package/src/mcp/checkSuite.ts +7 -10
  75. package/src/mcp/checkout.ts +13 -40
  76. package/src/mcp/crosswalk.ts +5 -0
  77. package/src/mcp/findingAudit.ts +1 -1
  78. package/src/mcp/git.ts +44 -2
  79. package/src/mcp/guardrails.ts +188 -36
  80. package/src/mcp/localContext.ts +2 -0
  81. package/src/mcp/localServer.ts +1 -0
  82. package/src/mcp/moduleExtraction.ts +17 -5
  83. package/src/mcp/moduleTests.ts +9 -2
  84. package/src/mcp/modules.ts +10 -7
  85. package/src/mcp/pr.ts +17 -65
  86. package/src/mcp/reproveSuggestions.ts +86 -0
  87. package/src/mcp/review.ts +249 -75
  88. package/src/mcp/reviewAnchor.ts +11 -23
  89. package/src/mcp/reviewCommentLimit.ts +79 -0
  90. package/src/mcp/reviewComments.ts +34 -81
  91. package/src/mcp/reviewDedup.ts +1 -1
  92. package/src/mcp/reviewMarkers.ts +25 -2
  93. package/src/mcp/reviewProvenance.ts +56 -21
  94. package/src/mcp/roots.ts +3 -2
  95. package/src/mcp/server.ts +2 -1
  96. package/src/mcp/shared.ts +40 -1
  97. package/src/mcp/shell.ts +27 -28
  98. package/src/mcp/suggestionProver.ts +8 -4
  99. package/src/mcp/terraform/checkovSeverities.ts +1156 -0
  100. package/src/mcp/terraform/concernResult.ts +2 -1
  101. package/src/mcp/terraform/consolidation.ts +232 -19
  102. package/src/mcp/terraform/cost.ts +16 -7
  103. package/src/mcp/terraform/cspm.ts +8 -4
  104. package/src/mcp/terraform/currency.ts +28 -12
  105. package/src/mcp/terraform/decisions.ts +1 -1
  106. package/src/mcp/terraform/deltaSummary.ts +50 -1
  107. package/src/mcp/terraform/emittedArtefacts.ts +63 -0
  108. package/src/mcp/terraform/evidence.ts +23 -3
  109. package/src/mcp/terraform/gateScan.ts +191 -128
  110. package/src/mcp/terraform/hcl.ts +27 -2
  111. package/src/mcp/terraform/idleFloor.ts +2 -1
  112. package/src/mcp/terraform/initArtefacts.ts +76 -0
  113. package/src/mcp/terraform/localModules.ts +112 -0
  114. package/src/mcp/terraform/moduleDocs.ts +2 -5
  115. package/src/mcp/terraform/moduleFindings.ts +102 -0
  116. package/src/mcp/terraform/moduleVersionConstraints.ts +1 -1
  117. package/src/mcp/terraform/planPairs.ts +2 -20
  118. package/src/mcp/terraform/policyGate.ts +0 -15
  119. package/src/mcp/terraform/refactor/attribution.ts +39 -5
  120. package/src/mcp/terraform/refactor/equivalence.ts +49 -10
  121. package/src/mcp/terraform/refactor/expressions.ts +323 -0
  122. package/src/mcp/terraform/refactor/references.ts +101 -0
  123. package/src/mcp/terraform/refactor/resources.ts +79 -7
  124. package/src/mcp/terraform/refactor/substitution.ts +852 -256
  125. package/src/mcp/terraform/scanDelta.ts +34 -0
  126. package/src/mcp/terraform/scanSession.ts +30 -0
  127. package/src/mcp/terraform/scannerCache.ts +4 -0
  128. package/src/mcp/terraform/scanners.ts +553 -233
  129. package/src/mcp/terraform/secretScan.ts +131 -0
  130. package/src/mcp/terraform/standardsReport.ts +14 -13
  131. package/src/mcp/terraform/tools/consolidationCandidates.ts +3 -0
  132. package/src/mcp/terraform/tools/emitEvidence.ts +13 -5
  133. package/src/mcp/terraform/tools/emitOscal.ts +3 -2
  134. package/src/mcp/terraform/tools/emitVex.ts +10 -4
  135. package/src/mcp/terraform/tools/equivalenceCheck.ts +139 -21
  136. package/src/mcp/terraform/tools/infracostDiff.ts +2 -2
  137. package/src/mcp/terraform/tools/plan.ts +30 -17
  138. package/src/mcp/terraform/tools/reviewScanDelta.ts +3 -0
  139. package/src/mcp/terraform/tools/scan.ts +5 -3
  140. package/src/mcp/terraform/tools/standardsReport.ts +1 -1
  141. package/src/mcp/terraform/tools/validate.ts +20 -5
  142. package/src/mcp/terraform/tools/verifyRemediation.ts +14 -7
  143. package/src/mcp/terraform/tools/versionCurrency.ts +3 -1
  144. package/src/mcp/terraform/treeLayout.ts +378 -0
  145. package/src/mcp/terraform/types.ts +4 -1
  146. package/src/mcp/terraform/versionRequirements.ts +11 -2
  147. package/src/modes/__snapshots__/assemblePrompt.test.ts.snap +2 -0
  148. package/src/modes/modernize-deprecated.ts +2 -2
  149. package/src/modes/refactor.ts +9 -9
  150. package/src/modes/remediate.ts +1 -1
  151. package/src/modes/terraform-code-review.ts +6 -3
  152. package/src/modes/update-dependencies.ts +6 -6
  153. package/src/phases/applyPlan.ts +33 -14
  154. package/src/phases/applySuggestions.ts +2 -2
  155. package/src/phases/runAgentWithWatchdogs.ts +2 -1
  156. package/src/toolState.ts +7 -0
  157. package/src/utils/agent/codexOAuth.ts +7 -7
  158. package/src/utils/agent/openCodeModels.ts +4 -6
  159. package/src/utils/cli.ts +1 -31
  160. package/src/utils/cloud/runContext.ts +1 -8
  161. package/src/utils/config/baseRefConfig.ts +6 -0
  162. package/src/utils/config/infraweaverConfig.ts +2 -0
  163. package/src/utils/config/payload.ts +41 -0
  164. package/src/utils/github/assets.ts +1 -6
  165. package/src/utils/github/github.ts +42 -150
  166. package/src/utils/github/token.ts +6 -11
  167. package/src/utils/log.ts +4 -5
  168. package/src/utils/markdownTable.ts +11 -0
  169. package/src/utils/prompt/changeImpact.ts +18 -26
  170. package/src/utils/prompt/instructions.ts +1 -1
  171. package/src/utils/prompt/promptDirectives.ts +1 -1
  172. package/src/utils/prompt/toolSelection.ts +11 -2
  173. package/src/utils/setup/toolLicensing.ts +7 -0
  174. package/src/utils/telemetry.ts +2 -1
  175. package/src/utils/unifiedDiff.ts +120 -0
@@ -1,14 +1,15 @@
1
- import { createSign } from "node:crypto";
2
1
  import { rename, writeFile } from "node:fs/promises";
3
2
  import { dirname, join } from "node:path";
4
3
  import * as core from "@actions/core";
5
4
  import { errorMessage } from "@infraweaver-io/core/errors";
6
5
  import { retry } from "@infraweaver-io/core/retry";
6
+ import { createAppAuth } from "@octokit/auth-app";
7
7
  import { throttling } from "@octokit/plugin-throttling";
8
8
  import { Octokit } from "@octokit/rest";
9
9
  import { DOC_LINKS, PRODUCT_NAME, PRODUCT_SLUG } from "#app/brand";
10
10
  import { apiFetch } from "#app/utils/cloud/apiFetch";
11
11
  import { cloudOidcAudience } from "#app/utils/cloud/apiUrl";
12
+ import { DEFAULT_CLOUD_TIMEOUT_MS } from "#app/utils/cloud/cloudFetchCore";
12
13
 
13
14
  function isObject(value: unknown) {
14
15
  return typeof value === "object" && value !== null;
@@ -32,37 +33,6 @@ interface InstallationToken {
32
33
  owner?: string;
33
34
  }
34
35
 
35
- interface GitHubAppConfig {
36
- appId: string;
37
- privateKey: string;
38
- repoOwner: string;
39
- repoName: string;
40
- }
41
-
42
- interface Installation {
43
- id: number;
44
- account: {
45
- login: string;
46
- type: string;
47
- };
48
- }
49
-
50
- interface Repository {
51
- owner: {
52
- login: string;
53
- };
54
- name: string;
55
- }
56
-
57
- interface InstallationTokenResponse {
58
- token: string;
59
- expires_at: string;
60
- }
61
-
62
- interface RepositoriesResponse {
63
- repositories: Repository[];
64
- }
65
-
66
36
  function isOIDCAvailable(): boolean {
67
37
  // OIDC requires both env vars to be set (only in real GitHub Actions with id-token permission)
68
38
  return Boolean(
@@ -176,11 +146,8 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
176
146
  }
177
147
  const reposParam = repos.length ? `?repos=${repos.join(",")}` : "";
178
148
 
179
- const timeoutMs = 30000;
180
- const controller = new AbortController();
181
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
182
-
183
149
  try {
150
+ // no `signal`: apiFetch applies the cloud transport's default timeout
184
151
  const tokenResponse = await apiFetch({
185
152
  path: `/api/github/installation-token${reposParam}`,
186
153
  method: "POST",
@@ -189,11 +156,8 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
189
156
  "Content-Type": "application/json",
190
157
  },
191
158
  body: opts?.permissions ? JSON.stringify({ permissions: opts.permissions }) : undefined,
192
- signal: controller.signal,
193
159
  });
194
160
 
195
- clearTimeout(timeoutId);
196
-
197
161
  if (!tokenResponse.ok) {
198
162
  // prefer the server-side `error` field — it's the single source of
199
163
  // truth for the install URL (uses GITHUB_APP_INSTALL_URL, or the
@@ -216,82 +180,23 @@ async function acquireTokenViaOIDC(opts?: AcquireTokenOptions): Promise<string>
216
180
  const tokenData = (await tokenResponse.json()) as InstallationToken;
217
181
  return tokenData.token;
218
182
  } catch (error) {
219
- clearTimeout(timeoutId);
220
-
221
- if (error instanceof Error && error.name === "AbortError") {
222
- throw new Error(`Token exchange timed out after ${timeoutMs}ms`);
183
+ // the default timeout's AbortSignal.timeout() rejects with a TimeoutError;
184
+ // the message carries "timed out" so acquireNewToken's retry policy matches it
185
+ if (error instanceof Error && error.name === "TimeoutError") {
186
+ throw new Error(`Token exchange timed out after ${DEFAULT_CLOUD_TIMEOUT_MS}ms`);
223
187
  }
224
188
  throw error;
225
189
  }
226
190
  }
227
191
 
228
- const base64UrlEncode = (str: string): string => {
229
- return Buffer.from(str)
230
- .toString("base64")
231
- .replace(/\+/g, "-")
232
- .replace(/\//g, "_")
233
- .replace(/=/g, "");
234
- };
235
-
236
- const generateJWT = (appId: string, privateKey: string): string => {
237
- const now = Math.floor(Date.now() / 1000);
238
- const payload = {
239
- iat: now - 60,
240
- exp: now + 5 * 60,
241
- iss: appId,
242
- };
243
-
244
- const header = {
245
- alg: "RS256",
246
- typ: "JWT",
247
- };
248
-
249
- const encodedHeader = base64UrlEncode(JSON.stringify(header));
250
- const encodedPayload = base64UrlEncode(JSON.stringify(payload));
251
- const signaturePart = `${encodedHeader}.${encodedPayload}`;
252
-
253
- const signature = createSign("RSA-SHA256")
254
- .update(signaturePart)
255
- .sign(privateKey, "base64")
256
- .replace(/\+/g, "-")
257
- .replace(/\//g, "_")
258
- .replace(/=/g, "");
259
-
260
- return `${signaturePart}.${signature}`;
261
- };
262
-
263
- const githubRequest = async <T>(
264
- path: string,
265
- options: {
266
- method?: string;
267
- headers?: Record<string, string>;
268
- body?: string;
269
- } = {},
270
- ): Promise<T> => {
271
- const { method = "GET", headers = {}, body } = options;
272
-
273
- const url = `https://api.github.com${path}`;
274
- const requestHeaders = {
275
- Accept: "application/vnd.github.v3+json",
276
- "User-Agent": `${PRODUCT_NAME}-Installation-Token-Generator/1.0`,
277
- ...headers,
278
- };
279
-
280
- const response = await fetch(url, {
281
- method,
282
- headers: requestHeaders,
283
- ...(body && { body }),
284
- signal: AbortSignal.timeout(30_000),
285
- });
286
-
287
- if (!response.ok) {
288
- const errorText = await response.text();
289
- throw new Error(
290
- `GitHub API request failed: ${response.status} ${response.statusText}\n${errorText}`,
291
- );
292
- }
293
-
294
- return response.json() as T;
192
+ // the local-dev App path talks to api.github.com directly; bound every request
193
+ // the way the cloud transport bounds its own.
194
+ const appRequestDefaults = {
195
+ userAgent: `${PRODUCT_NAME}-Installation-Token-Generator/1.0`,
196
+ request: {
197
+ fetch: (url: string, init: RequestInit) =>
198
+ fetch(url, { ...init, signal: AbortSignal.timeout(30_000) }),
199
+ },
295
200
  };
296
201
 
297
202
  const checkRepositoryAccess = async (
@@ -300,13 +205,13 @@ const checkRepositoryAccess = async (
300
205
  repoName: string,
301
206
  ): Promise<boolean> => {
302
207
  try {
303
- const response = await githubRequest<RepositoriesResponse>("/installation/repositories", {
304
- headers: { Authorization: `token ${token}` },
305
- });
208
+ const { data } = await new Octokit({ ...appRequestDefaults, auth: token }).request(
209
+ "GET /installation/repositories",
210
+ );
306
211
 
307
212
  const ownerLower = repoOwner.toLowerCase();
308
213
  const nameLower = repoName.toLowerCase();
309
- return response.repositories.some(
214
+ return data.repositories.some(
310
215
  (repo) =>
311
216
  repo.owner.login.toLowerCase() === ownerLower && repo.name.toLowerCase() === nameLower,
312
217
  );
@@ -316,31 +221,18 @@ const checkRepositoryAccess = async (
316
221
  };
317
222
 
318
223
  const createInstallationToken = async (
319
- jwt: string,
224
+ app: Octokit,
320
225
  installationId: number,
321
226
  permissions?: GitHubAppPermissions,
322
227
  ): Promise<string> => {
323
- const requestOpts: {
324
- method: string;
325
- headers: Record<string, string>;
326
- body?: string;
327
- } = {
328
- method: "POST",
329
- headers: { Authorization: `Bearer ${jwt}` },
228
+ const { token } = (await app.auth({ type: "installation", installationId, permissions })) as {
229
+ token: string;
330
230
  };
331
- if (permissions) {
332
- requestOpts.body = JSON.stringify({ permissions });
333
- }
334
- const response = await githubRequest<InstallationTokenResponse>(
335
- `/app/installations/${installationId}/access_tokens`,
336
- requestOpts,
337
- );
338
-
339
- return response.token;
231
+ return token;
340
232
  };
341
233
 
342
234
  const findInstallationId = async (
343
- jwt: string,
235
+ app: Octokit,
344
236
  repoOwner: string,
345
237
  repoName: string,
346
238
  ): Promise<number> => {
@@ -352,10 +244,10 @@ const findInstallationId = async (
352
244
  // repo, and the loop's per-installation errors are what produce the useful
353
245
  // diagnosis in that case.
354
246
  try {
355
- const direct = await githubRequest<{ id: number }>(
356
- `/repos/${repoOwner}/${repoName}/installation`,
357
- { headers: { Authorization: `Bearer ${jwt}` } },
358
- );
247
+ const { data: direct } = await app.request("GET /repos/{owner}/{repo}/installation", {
248
+ owner: repoOwner,
249
+ repo: repoName,
250
+ });
359
251
  if (Number.isInteger(direct?.id)) return direct.id;
360
252
  } catch {
361
253
  // Not installed on this repo, or the endpoint is unavailable — fall through
@@ -363,9 +255,7 @@ const findInstallationId = async (
363
255
  // selection includes it and which produces the better error message.
364
256
  }
365
257
 
366
- const installations = await githubRequest<Installation[]>("/app/installations", {
367
- headers: { Authorization: `Bearer ${jwt}` },
368
- });
258
+ const { data: installations } = await app.request("GET /app/installations");
369
259
 
370
260
  // Probing each installation is best-effort — one that legitimately can't mint a
371
261
  // token (suspended, revoked) must not abort the search. But swallowing the error
@@ -376,7 +266,7 @@ const findInstallationId = async (
376
266
  let lastError: unknown;
377
267
  for (const installation of installations) {
378
268
  try {
379
- const tempToken = await createInstallationToken(jwt, installation.id);
269
+ const tempToken = await createInstallationToken(app, installation.id);
380
270
  const hasAccess = await checkRepositoryAccess(tempToken, repoOwner, repoName);
381
271
 
382
272
  if (hasAccess) {
@@ -407,16 +297,18 @@ async function acquireTokenViaGitHubApp(opts?: AcquireTokenOptions): Promise<str
407
297
 
408
298
  const repoContext = parseRepoContext();
409
299
 
410
- const config: GitHubAppConfig = {
411
- appId: process.env.GITHUB_APP_ID,
412
- privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
413
- repoOwner: repoContext.owner,
414
- repoName: repoContext.name,
415
- };
416
-
417
- const jwt = generateJWT(config.appId, config.privateKey);
418
- const installationId = await findInstallationId(jwt, config.repoOwner, config.repoName);
419
- return await createInstallationToken(jwt, installationId, opts?.permissions);
300
+ // createAppAuth signs the App JWT for the /app/* and /repos/{owner}/{repo}/installation
301
+ // routes and mints installation tokens through `app.auth()`.
302
+ const app = new Octokit({
303
+ ...appRequestDefaults,
304
+ authStrategy: createAppAuth,
305
+ auth: {
306
+ appId: process.env.GITHUB_APP_ID,
307
+ privateKey: process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"),
308
+ },
309
+ });
310
+ const installationId = await findInstallationId(app, repoContext.owner, repoContext.name);
311
+ return await createInstallationToken(app, installationId, opts?.permissions);
420
312
  }
421
313
 
422
314
  /**
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import * as core from "@actions/core";
3
3
  import { errorMessage } from "@infraweaver-io/core/errors";
4
4
  import { onExitSignal } from "@infraweaver-io/sandbox/exitHandler";
5
+ import { Octokit } from "@octokit/rest";
5
6
  import type { PushPermission } from "#app/external";
6
7
  import { log } from "#app/utils/cli";
7
8
  import { stashKeylessOidcCreds } from "#app/utils/cloud/keylessOidc";
@@ -260,18 +261,12 @@ export function getGitHubInstallationToken(): string {
260
261
  }
261
262
 
262
263
  export async function revokeGitHubInstallationToken(token: string): Promise<void> {
263
- const apiUrl = process.env.GITHUB_API_URL || "https://api.github.com";
264
-
265
264
  try {
266
- await fetch(`${apiUrl}/installation/token`, {
267
- method: "DELETE",
268
- headers: {
269
- Accept: "application/vnd.github+json",
270
- Authorization: `Bearer ${token}`,
271
- "X-GitHub-Api-Version": "2022-11-28",
272
- },
273
- signal: AbortSignal.timeout(10_000),
274
- });
265
+ await new Octokit({
266
+ auth: token,
267
+ baseUrl: process.env.GITHUB_API_URL || "https://api.github.com",
268
+ request: { signal: AbortSignal.timeout(10_000) },
269
+ }).rest.apps.revokeInstallationAccessToken();
275
270
  log.debug("» installation token revoked");
276
271
  } catch (error) {
277
272
  log.info(`Failed to revoke installation token: ${errorMessage(error)}`);
package/src/utils/log.ts CHANGED
@@ -6,8 +6,10 @@ import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import * as core from "@actions/core";
7
7
  import { setLogger } from "@infraweaver-io/core/logger";
8
8
  import { type AgentUsage, formatCostUsd } from "@infraweaver-io/models/usage";
9
+ import pc from "picocolors";
9
10
  import { table } from "table";
10
11
  import { isGitHubActions, isInsideDocker } from "#app/utils/globals";
12
+ import { tableCell } from "#app/utils/markdownTable";
11
13
  import { redactSecrets } from "#app/utils/redact";
12
14
 
13
15
  // --- log prefix via AsyncLocalStorage ---
@@ -16,9 +18,6 @@ type LogContext = { prefix: string };
16
18
 
17
19
  const logContext = new AsyncLocalStorage<LogContext>();
18
20
 
19
- const MAGENTA = "\x1b[35m";
20
- const RESET = "\x1b[0m";
21
-
22
21
  /** run `fn` with every log line prefixed by `prefix` (e.g. "[task-label]") in magenta */
23
22
  export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<T> {
24
23
  return logContext.run({ prefix }, fn);
@@ -27,7 +26,7 @@ export function withLogPrefix<T>(prefix: string, fn: () => Promise<T>): Promise<
27
26
  function prefixLines(message: string): string {
28
27
  const ctx = logContext.getStore();
29
28
  if (!ctx) return message;
30
- const colored = `${MAGENTA}${ctx.prefix}${RESET} `;
29
+ const colored = `${pc.magenta(ctx.prefix)} `;
31
30
  return message
32
31
  .split("\n")
33
32
  .map((line) => `${colored}${line}`)
@@ -439,7 +438,7 @@ export function formatUsageSummary(entries: AgentUsage[]): string {
439
438
 
440
439
  const rows = entries.map(
441
440
  (e) =>
442
- `| ${e.agent} | ${fmt(nonCachedInput(e))} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} | ${fmt(e.outputTokens)} | ${fmt(totalFor(e))} | ${costCell(e)} |`,
441
+ `| ${tableCell(e.agent)} | ${fmt(nonCachedInput(e))} | ${fmt(e.cacheReadTokens ?? 0)} | ${fmt(e.cacheWriteTokens ?? 0)} | ${fmt(e.outputTokens)} | ${fmt(totalFor(e))} | ${costCell(e)} |`,
443
442
  );
444
443
 
445
444
  const totalsRows: string[] = [];
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Flatten text into one markdown table cell: escape pipes (an unescaped `|`
3
+ * starts a new column and shears the row) and fold newlines, which would end the
4
+ * row, into single spaces.
5
+ */
6
+ export function tableCell(text: string): string {
7
+ return text
8
+ .replaceAll("|", "\\|")
9
+ .replaceAll(/\s*\n\s*/g, " ")
10
+ .trim();
11
+ }
@@ -2,6 +2,7 @@ import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { $ } from "@infraweaver-io/sandbox/shell";
4
4
  import { log } from "#app/utils/cli";
5
+ import { parseDiffLines } from "#app/utils/unifiedDiff";
5
6
 
6
7
  /**
7
8
  * Deterministic change-impact leads for Terraform review.
@@ -75,27 +76,27 @@ const TF_DECL_PATTERNS: Array<{
75
76
  }> = [
76
77
  {
77
78
  declaration: "resource",
78
- re: /^resource\s+"([A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/,
79
+ re: /^resource\s+"?([A-Za-z0-9_]+)"?\s+"?([A-Za-z0-9_-]+)"?/,
79
80
  build: (m) => ({ label: `${m[1]}.${m[2]}`, ref: `${m[1]}.${m[2]}` }),
80
81
  },
81
82
  {
82
83
  declaration: "data",
83
- re: /^data\s+"([A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/,
84
+ re: /^data\s+"?([A-Za-z0-9_]+)"?\s+"?([A-Za-z0-9_-]+)"?/,
84
85
  build: (m) => ({ label: `data.${m[1]}.${m[2]}`, ref: `data.${m[1]}.${m[2]}` }),
85
86
  },
86
87
  {
87
88
  declaration: "variable",
88
- re: /^variable\s+"([A-Za-z0-9_-]+)"/,
89
+ re: /^variable\s+"?([A-Za-z0-9_-]+)"?/,
89
90
  build: (m) => ({ label: `var.${m[1]}`, ref: `var.${m[1]}` }),
90
91
  },
91
92
  {
92
93
  declaration: "module",
93
- re: /^module\s+"([A-Za-z0-9_-]+)"/,
94
+ re: /^module\s+"?([A-Za-z0-9_-]+)"?/,
94
95
  build: (m) => ({ label: `module.${m[1]}`, ref: `module.${m[1]}` }),
95
96
  },
96
97
  {
97
98
  declaration: "output",
98
- re: /^output\s+"([A-Za-z0-9_-]+)"/,
99
+ re: /^output\s+"?([A-Za-z0-9_-]+)"?/,
99
100
  // an output is consumed as `module.<caller>.<name>`; grep that access form as
100
101
  // a regex rather than the bare `.<name>`, which would match every attribute
101
102
  // access named `<name>` in the tree (for common names like `id`/`arn` that is
@@ -143,34 +144,25 @@ export function extractTerraformAtoms(files: ChangedFile[]): {
143
144
  if (!file.patch) continue;
144
145
  // only Terraform declarations matter here.
145
146
  if (!/\.(tf|tfvars)$/.test(file.filename)) continue;
146
- let oldLine = 0;
147
- let newLine = 0;
148
- for (const patchLine of file.patch.split("\n")) {
149
- const hunk = patchLine.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
150
- if (hunk) {
151
- oldLine = Number.parseInt(hunk[1] ?? "0", 10);
152
- newLine = Number.parseInt(hunk[2] ?? "0", 10);
153
- continue;
154
- }
155
- if (patchLine.startsWith("+")) {
156
- const decl = declarationOf(patchLine.slice(1));
157
- const where: ChangeLocation = { path: file.filename, line: newLine, kind: "added" };
158
- addedLines.add(locationKey(file.filename, newLine));
147
+ for (const patchLine of parseDiffLines(file.patch)) {
148
+ if (patchLine.kind === "add") {
149
+ const decl = declarationOf(patchLine.text);
150
+ const where: ChangeLocation = {
151
+ path: file.filename,
152
+ line: patchLine.newLine,
153
+ kind: "added",
154
+ };
155
+ addedLines.add(locationKey(file.filename, patchLine.newLine));
159
156
  if (decl && !byRef.has(decl.ref)) byRef.set(decl.ref, { ...decl, where });
160
- newLine++;
161
- } else if (patchLine.startsWith("-")) {
162
- const decl = declarationOf(patchLine.slice(1));
157
+ } else if (patchLine.kind === "remove") {
158
+ const decl = declarationOf(patchLine.text);
163
159
  const oldPath = file.previous_filename ?? file.filename;
164
160
  if (decl && !byRef.has(decl.ref)) {
165
161
  byRef.set(decl.ref, {
166
162
  ...decl,
167
- where: { path: oldPath, line: oldLine, kind: "removed" },
163
+ where: { path: oldPath, line: patchLine.oldLine, kind: "removed" },
168
164
  });
169
165
  }
170
- oldLine++;
171
- } else if (!patchLine.startsWith("\\")) {
172
- oldLine++;
173
- newLine++;
174
166
  }
175
167
  }
176
168
  }
@@ -421,7 +421,7 @@ When embedding images (e.g. uploaded screenshots) in comments or PR bodies, alwa
421
421
 
422
422
  **Your raw assistant messages are never delivered** — they exist only in the run logs. Anything the user is meant to see (an answer to a question, a mention reply, a result) MUST go through \`report_progress\` (or another ${infraweaverMcpName} write tool). Do not rely on returning the answer as plain text — the harness makes a best-effort attempt to recover it into the progress comment, but that is a safety net, not a substitute for calling \`report_progress\`.
423
423
 
424
- **\`report_progress\`**: call this exactly once at the end of every run with a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") — the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise — do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
424
+ **\`report_progress\`**: call this exactly once at the end of every run, with its \`body\` set to a brief final summary (1-3 sentences) unless the mode guidance instructs otherwise. Never call it for intermediate status updates (e.g., "Checking for changes...", "Starting review...") — the task list handles live progress automatically. Calling \`report_progress\` replaces the task list with your summary and preserves the current task list in a collapsible section. Keep the summary concise — do not repeat what the task list already shows. Focus on the outcome (what was accomplished, links to artifacts) rather than listing individual steps. If something failed, include the tool's error text even when that makes the summary longer.
425
425
 
426
426
  Never use \`create_issue_comment\` for task progress — that creates duplicate comments and leaves the progress comment stuck in its initial state. \`create_issue_comment\` is only for standalone comments unrelated to your current task. Plan output (initial post AND revisions) goes through \`report_progress\` — see the Plan mode guidance for details.
427
427
 
@@ -116,7 +116,7 @@ const REFACTOR_SOURCE_DIRECTIVES: Readonly<Record<RefactorSource, string>> = {
116
116
  ].join("\n"),
117
117
  "same-repo": [
118
118
  "Use ONLY a module that already exists in THIS repository (variant b.2 — a local",
119
- "`./modules/x` path, candidate `kind: local`). Do not author a new module, reach into",
119
+ "module called by its `call_source`, candidate `kind: local`). Do not author a new module, reach into",
120
120
  "another repo, or adopt a registry module. If no suitable in-repo module matches the",
121
121
  "cluster, report that and stop — do not fall back to another variant.",
122
122
  ].join("\n"),
@@ -55,7 +55,7 @@ export interface ToolSelectionFlags {
55
55
  * via its dedicated input. Listing keeps their long-standing behaviour intact
56
56
  * while the unified list can also enable/disable them.
57
57
  */
58
- const FLAG_OPT_IN: ReadonlySet<ToolId> = new Set<ToolId>(["gitleaks", "terratest"]);
58
+ const FLAG_OPT_IN: ReadonlySet<ToolId> = new Set<ToolId>(["gitleaks", "betterleaks", "terratest"]);
59
59
 
60
60
  /** map a token to a canonical tool id (case-insensitive; common spellings). */
61
61
  const TOKEN_ALIASES: Readonly<Record<string, ToolId>> = {
@@ -69,6 +69,7 @@ const TOKEN_ALIASES: Readonly<Record<string, ToolId>> = {
69
69
  infracost: "infracost",
70
70
  cost: "infracost",
71
71
  gitleaks: "gitleaks",
72
+ betterleaks: "betterleaks",
72
73
  conftest: "conftest",
73
74
  opa: "conftest",
74
75
  policy: "conftest",
@@ -177,7 +178,13 @@ function decide(id: ToolId, flags: ToolSelectionFlags): Verdict {
177
178
  if (FLAG_OPT_IN.has(id)) {
178
179
  return flagFor(id, flags)
179
180
  ? { on: true }
180
- : { on: false, reason: `opt-in — set the ${id} input or name "${id}" in tools_enabled` };
181
+ : {
182
+ on: false,
183
+ reason:
184
+ id === "betterleaks"
185
+ ? `opt-in — name "${id}" in tools_enabled`
186
+ : `opt-in — set the ${id} input or name "${id}" in tools_enabled`,
187
+ };
181
188
  }
182
189
  // permissive default-on (subject to the tool's own runtime presence checks).
183
190
  return { on: true };
@@ -235,6 +242,8 @@ export function scannerToolId(source: string): ToolId | null {
235
242
  return "trivy";
236
243
  case "checkov":
237
244
  return "checkov";
245
+ case "betterleaks":
246
+ return "betterleaks";
238
247
  default:
239
248
  return null;
240
249
  }
@@ -20,6 +20,7 @@ export type ToolId =
20
20
  | "checkov"
21
21
  | "infracost"
22
22
  | "gitleaks"
23
+ | "betterleaks"
23
24
  | "conftest"
24
25
  | "terratest"
25
26
  | "terratag"
@@ -96,6 +97,12 @@ export const TOOL_LICENSES: Readonly<Record<ToolId, ToolLicense>> = {
96
97
  license: "MIT",
97
98
  class: "permissive",
98
99
  },
100
+ betterleaks: {
101
+ id: "betterleaks",
102
+ name: "Betterleaks",
103
+ license: "MIT",
104
+ class: "permissive",
105
+ },
99
106
  conftest: {
100
107
  id: "conftest",
101
108
  name: "Conftest (OPA)",
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { errorMessage } from "@infraweaver-io/core/errors";
2
3
  import { PRODUCT_SLUG } from "#app/brand";
3
4
  import { log } from "#app/utils/cli";
4
5
  import { redactSecrets } from "#app/utils/redact";
@@ -356,6 +357,6 @@ export async function flushTelemetry(): Promise<void> {
356
357
  }
357
358
  log.debug(`telemetry exported ${armed.spans.length} span(s)`);
358
359
  } catch (err) {
359
- log.debug(`telemetry export failed: ${err instanceof Error ? err.message : String(err)}`);
360
+ log.debug(`telemetry export failed: ${errorMessage(err)}`);
360
361
  }
361
362
  }