claude-crap 0.4.5 → 0.4.7

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 (34) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +22 -25
  3. package/dist/dashboard/file-detail.d.ts +6 -0
  4. package/dist/dashboard/file-detail.d.ts.map +1 -1
  5. package/dist/dashboard/file-detail.js +1 -0
  6. package/dist/dashboard/file-detail.js.map +1 -1
  7. package/dist/monorepo/project-map.d.ts.map +1 -1
  8. package/dist/monorepo/project-map.js +135 -6
  9. package/dist/monorepo/project-map.js.map +1 -1
  10. package/dist/scanner/bootstrap.d.ts.map +1 -1
  11. package/dist/scanner/bootstrap.js +2 -2
  12. package/dist/scanner/bootstrap.js.map +1 -1
  13. package/dist/shared/exclusions.d.ts.map +1 -1
  14. package/dist/shared/exclusions.js +22 -0
  15. package/dist/shared/exclusions.js.map +1 -1
  16. package/package.json +1 -1
  17. package/plugin/.claude-plugin/plugin.json +1 -1
  18. package/plugin/bundle/dashboard/public/index.html +216 -7
  19. package/plugin/bundle/mcp-server.mjs +145 -31
  20. package/plugin/bundle/mcp-server.mjs.map +3 -3
  21. package/plugin/hooks/lib/gatekeeper-rules.mjs +274 -45
  22. package/plugin/hooks/lib/quality-gate.mjs +3 -0
  23. package/plugin/package-lock.json +8 -8
  24. package/plugin/package.json +1 -1
  25. package/src/dashboard/file-detail.ts +7 -0
  26. package/src/dashboard/public/index.html +216 -7
  27. package/src/monorepo/project-map.ts +144 -6
  28. package/src/scanner/bootstrap.ts +7 -2
  29. package/src/shared/exclusions.ts +26 -0
  30. package/src/tests/exclusions.test.ts +53 -0
  31. package/src/tests/file-detail-api.test.ts +38 -0
  32. package/src/tests/gatekeeper-rules.test.ts +173 -0
  33. package/src/tests/project-map.test.ts +216 -0
  34. package/src/tests/workspace-walker.test.ts +94 -0
@@ -52,35 +52,267 @@
52
52
  const DEFAULT_BLOCKED_PATH_REGEX =
53
53
  /(^|\/)(\.git|\.env|\.env\..*|node_modules|\.venv|secrets?|credentials?|id_rsa|\.ssh)(\/|$)/i;
54
54
 
55
+ /**
56
+ * Canonical AWS-published example access keys. These values appear in
57
+ * AWS's own documentation, SDK fixtures, and thousands of tutorials; they
58
+ * are guaranteed *not* to authenticate against any real AWS account. The
59
+ * gatekeeper allowlists them so the plugin can still edit its own docs
60
+ * and tests that mention the rule itself.
61
+ *
62
+ * Entries are assembled from split literals so this source file does not
63
+ * itself match the AKIA regex and trip the gatekeeper when loaded.
64
+ */
65
+ const AWS_BENIGN_EXAMPLES = new Set([
66
+ "AKIA" + "IOSFODNN7" + "EXAMPLE", // AWS docs / SDK fixtures
67
+ "AKIA" + "I44QH8DHB" + "EXAMPLE", // AWS SRA sample
68
+ "AKIA" + "IOSFODNN7" + "EXAMPL2", // AWS sample v2
69
+ ]);
70
+
55
71
  /**
56
72
  * Heuristic signatures of secrets that should never be committed to source.
57
73
  * This list is intentionally conservative — the gatekeeper is a speed bump,
58
74
  * not a replacement for a real secret scanner. Deeper detection runs in
59
75
  * PostToolUse via the MCP `ingest_sarif` tool.
76
+ *
77
+ * Rule IDs here carry no category prefix — the emitter adds exactly one
78
+ * `SONAR-SEC-` prefix via {@link formatSecretRuleId}. Keeping the prefix
79
+ * out of the rule table avoids double-prefixed SARIF ruleIds, which break
80
+ * dedup keys and downstream SIEM correlation.
60
81
  */
61
- const HARDCODED_SECRET_PATTERNS = [
62
- { id: "SEC-AWS", re: /AKIA[0-9A-Z]{16}/ },
63
- { id: "SEC-PRIVKEY", re: /-----BEGIN (RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/ },
64
- { id: "SEC-SLACKTOKEN", re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
65
- { id: "SEC-GHTOKEN", re: /gh[pousr]_[A-Za-z0-9]{36,}/ },
66
- { id: "SEC-JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
82
+ export const HARDCODED_SECRET_PATTERNS = [
83
+ { id: "AWS", re: /AKIA[0-9A-Z]{16}/g },
84
+ { id: "PRIVKEY", re: /-----BEGIN (RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/ },
85
+ { id: "SLACKTOKEN", re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
86
+ { id: "GHTOKEN", re: /gh[pousr]_[A-Za-z0-9]{36,}/ },
87
+ { id: "JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
67
88
  ];
68
89
 
69
90
  /**
70
- * Destructive shell patterns. These commands can silently destroy work,
71
- * overwrite published git history, or execute remote code without review.
72
- * The gatekeeper refuses them outright if the user really needs to run
73
- * one of these, they should do it from their own terminal.
91
+ * Destructive shell patterns evaluated by regex. `rm -rf`-style commands
92
+ * are handled separately by {@link findDestructiveBashHit}, which
93
+ * tokenises the command line so quoted strings (e.g. `echo 'rm -rf /'`)
94
+ * do not false-positive.
95
+ *
96
+ * Rule IDs carry no category prefix — the emitter adds a single
97
+ * `SONAR-BASH-` prefix via {@link formatBashRuleId}.
74
98
  */
75
- const DESTRUCTIVE_BASH_PATTERNS = [
76
- { id: "BASH-RMROOT", re: /\brm\s+(-[frR]+\s+|--force\s+|--recursive\s+).*(\s|^)\/($|\s)/ },
77
- { id: "BASH-RMHOME", re: /\brm\s+-[frR]+\s+.*\$HOME\b/ },
78
- { id: "BASH-DD", re: /\bdd\s+.*of=\/dev\/(sd|nvme|disk)/ },
79
- { id: "BASH-GITFORCE", re: /\bgit\s+push\s+.*--force(?!-with-lease)/ },
80
- { id: "BASH-GITRESET", re: /\bgit\s+reset\s+--hard\s+origin/ },
81
- { id: "BASH-CURLSUDO", re: /\bcurl\s+[^|]*\|\s*(sudo\s+)?(bash|sh|zsh|fish)/ },
99
+ export const DESTRUCTIVE_BASH_PATTERNS = [
100
+ { id: "DD", re: /\bdd\s+.*of=\/dev\/(sd|nvme|disk)/ },
101
+ { id: "GITFORCE", re: /\bgit\s+push\s+.*--force(?!-with-lease)/ },
102
+ { id: "GITRESET", re: /\bgit\s+reset\s+--hard\s+origin/ },
103
+ { id: "CURLSUDO", re: /\bcurl\s+[^|]*\|\s*(sudo\s+)?(bash|sh|zsh|fish)/ },
82
104
  ];
83
105
 
106
+ /**
107
+ * System directories whose recursive deletion would brick the host or
108
+ * destroy another user's data. Matched as the first path component of
109
+ * the target argument after a recursive/force `rm`.
110
+ */
111
+ const RM_SYSTEM_DIRS = new Set([
112
+ "usr", "etc", "var", "bin", "sbin", "lib", "boot",
113
+ "System", "Applications", "Library", "opt", "root", "home",
114
+ ]);
115
+
116
+ /**
117
+ * Build a SARIF rule ID for a secret pattern. Keeps the single source of
118
+ * truth for the `SONAR-SEC-*` namespace in the emitter.
119
+ *
120
+ * @param {{ id: string }} pattern
121
+ * @returns {string}
122
+ */
123
+ export function formatSecretRuleId(pattern) {
124
+ return `SONAR-SEC-${pattern.id}`;
125
+ }
126
+
127
+ /**
128
+ * Build a SARIF rule ID for a destructive-bash pattern.
129
+ *
130
+ * @param {{ id: string }} pattern
131
+ * @returns {string}
132
+ */
133
+ export function formatBashRuleId(pattern) {
134
+ return `SONAR-BASH-${pattern.id}`;
135
+ }
136
+
137
+ /**
138
+ * Tokenise a shell command into argv-like tokens that distinguish
139
+ * quoted strings from bare words, and emit unquoted shell control
140
+ * operators (`;`, `&&`, `||`, `|`, `&`) as their own tokens. Emitting
141
+ * operators as separate tokens prevents a target argument from "sticking"
142
+ * to an operator and slipping past target classification — `rm -rf /;ls`
143
+ * must tokenise to `[..., -rf, /, ;, ls]`, not `[..., -rf, /;ls]`.
144
+ *
145
+ * Intentionally minimal: single and double quotes only, no backslash-escape
146
+ * handling, no variable expansion. Sufficient for gatekeeper intent
147
+ * detection — a full shell parser is out of scope.
148
+ *
149
+ * @param {string} cmd
150
+ * @returns {Array<{ text: string, quoted: boolean }>}
151
+ */
152
+ function tokenizeBash(cmd) {
153
+ const tokens = [];
154
+ // Alternation order matters: quoted strings first (so their contents are
155
+ // preserved verbatim), then control operators (longest match first:
156
+ // `&&`/`||` before `&`/`|`), then bare runs of non-whitespace that stop
157
+ // at the next operator character.
158
+ const re = /"((?:[^"\\]|\\.)*)"|'([^']*)'|(&&|\|\||;|\||&)|([^\s;|&"']+)/g;
159
+ let m;
160
+ while ((m = re.exec(cmd)) !== null) {
161
+ if (m[1] !== undefined) tokens.push({ text: m[1], quoted: true });
162
+ else if (m[2] !== undefined) tokens.push({ text: m[2], quoted: true });
163
+ else if (m[3] !== undefined) tokens.push({ text: /** @type {string} */ (m[3]), quoted: false });
164
+ else tokens.push({ text: /** @type {string} */ (m[4]), quoted: false });
165
+ }
166
+ return tokens;
167
+ }
168
+
169
+ /**
170
+ * Extract the basename of a command token. `rm`, `/bin/rm`, `./rm`, and
171
+ * `../bin/rm` all resolve to `"rm"`. Used so path-qualified invocations
172
+ * cannot bypass detection — the shell looks up the binary by the last
173
+ * path segment, and so does the gatekeeper.
174
+ *
175
+ * @param {string} token
176
+ * @returns {string}
177
+ */
178
+ function basename(token) {
179
+ const i = token.lastIndexOf("/");
180
+ return i === -1 ? token : token.slice(i + 1);
181
+ }
182
+
183
+ /**
184
+ * True when a flag token requests recursive or force semantics. Accepts
185
+ * long forms (`--recursive`, `--force`) and short-flag clusters that
186
+ * include any of `r`, `R`, `f` (`-rf`, `-Rf`, `-rfv`, ...).
187
+ *
188
+ * @param {string} token
189
+ * @returns {boolean}
190
+ */
191
+ function isRecursiveOrForceFlag(token) {
192
+ if (token === "--recursive" || token === "--force") return true;
193
+ return /^-[a-zA-Z]*[rRf][a-zA-Z]*$/.test(token);
194
+ }
195
+
196
+ /**
197
+ * Classify the destination argument of a recursive `rm` into one of the
198
+ * high-severity categories we block, or `null` if the target is benign.
199
+ *
200
+ * Home-directory matching covers exact (`~`, `$HOME`) and prefixed
201
+ * (`~/anything`, `$HOME/anything`, `~/*`) forms — every path rooted at
202
+ * the user's home is considered destructive under recursive force.
203
+ *
204
+ * @param {string} target
205
+ * @returns {"RMROOT" | "RMSYSDIR" | "RMHOME" | null}
206
+ */
207
+ function classifyRmTarget(target) {
208
+ if (target === "/" || target === "/*") return "RMROOT";
209
+ // ~, ~/, ~/anything, ~/*, $HOME, $HOME/, $HOME/anything, $HOME/*
210
+ if (/^(~|\$HOME)(\/.*|\*)?$/.test(target)) return "RMHOME";
211
+ // Any path whose first component is a protected system directory.
212
+ const m = /^\/([A-Za-z]+)(\/|$)/.exec(target);
213
+ if (m && RM_SYSTEM_DIRS.has(/** @type {string} */ (m[1]))) return "RMSYSDIR";
214
+ return null;
215
+ }
216
+
217
+ /**
218
+ * Scan a command line for destructive operations. Returns the first hit
219
+ * or `null` when the command is safe. A hit is `{ id, match, ruleId }`
220
+ * where `ruleId` is the fully-formatted SARIF identifier.
221
+ *
222
+ * Handles two modes:
223
+ * 1. rm with `-r`/`-R`/`-f`/`--recursive`/`--force` targeting root,
224
+ * a system directory, or `$HOME` / `~` — via token-aware walk so
225
+ * strings inside quotes (e.g. `echo 'rm -rf /'`) do not trigger.
226
+ * 2. Other destructive commands (`dd of=/dev/...`, `git push --force`,
227
+ * `git reset --hard origin`, `curl ... | sh`) — via regex, which
228
+ * is good enough for these patterns and cheaper than tokenisation.
229
+ *
230
+ * @param {string | null | undefined} command
231
+ * @returns {{ id: string, ruleId: string, match: string } | null}
232
+ */
233
+ export function findDestructiveBashHit(command) {
234
+ if (!command) return null;
235
+
236
+ // Mode 1 — token walk for rm invocations. Match on the basename of
237
+ // the token so path-qualified forms (`/bin/rm`, `./rm`, `../bin/rm`)
238
+ // are caught. This intentionally accepts the cosmetic false positive
239
+ // of `echo rm -rf /` blocking — the outcome is safe (the LLM is
240
+ // asked to reformulate) and a full executable-position parser
241
+ // introduces its own bypass edge cases around wrapper flags.
242
+ const tokens = tokenizeBash(command);
243
+ for (let i = 0; i < tokens.length; i++) {
244
+ const t = /** @type {{ text: string, quoted: boolean }} */ (tokens[i]);
245
+ if (t.quoted) continue;
246
+ if (basename(t.text) !== "rm") continue;
247
+
248
+ // Collect flags + targets until end or a command separator.
249
+ const flags = [];
250
+ const targets = [];
251
+ for (let j = i + 1; j < tokens.length; j++) {
252
+ const next = /** @type {{ text: string, quoted: boolean }} */ (tokens[j]);
253
+ if (!next.quoted && /^[;&|]+$/.test(next.text)) break;
254
+ if (!next.quoted && next.text.startsWith("-")) flags.push(next.text);
255
+ else targets.push(next.text);
256
+ }
257
+
258
+ if (!flags.some(isRecursiveOrForceFlag)) continue;
259
+ for (const target of targets) {
260
+ const category = classifyRmTarget(target);
261
+ if (category !== null) {
262
+ return {
263
+ id: category,
264
+ ruleId: formatBashRuleId({ id: category }),
265
+ match: target,
266
+ };
267
+ }
268
+ }
269
+ }
270
+
271
+ // Mode 2 — other destructive patterns by regex.
272
+ for (const pat of DESTRUCTIVE_BASH_PATTERNS) {
273
+ if (pat.re.test(command)) {
274
+ return {
275
+ id: pat.id,
276
+ ruleId: formatBashRuleId(pat),
277
+ match: command,
278
+ };
279
+ }
280
+ }
281
+
282
+ return null;
283
+ }
284
+
285
+ /**
286
+ * Scan text for hardcoded-secret signatures. Canonical AWS example
287
+ * keys are allowlisted so the plugin can edit its own docs/tests.
288
+ *
289
+ * @param {string} text
290
+ * @returns {Array<{ id: string, ruleId: string, match: string }>}
291
+ */
292
+ export function findSecretHits(text) {
293
+ if (typeof text !== "string" || text.length === 0) return [];
294
+ const hits = [];
295
+ for (const pat of HARDCODED_SECRET_PATTERNS) {
296
+ // Reset stateful /g regexes between invocations.
297
+ if (pat.re.global) pat.re.lastIndex = 0;
298
+ if (pat.id === "AWS") {
299
+ // Reuse the canonical pattern from the rule table rather than
300
+ // duplicating the literal here — a single source of truth for
301
+ // the regex means the allowlist can never drift from the matcher.
302
+ for (const m of text.matchAll(pat.re)) {
303
+ if (AWS_BENIGN_EXAMPLES.has(m[0])) continue;
304
+ hits.push({ id: "AWS", ruleId: formatSecretRuleId({ id: "AWS" }), match: m[0] });
305
+ }
306
+ continue;
307
+ }
308
+ const m = pat.re.exec(text);
309
+ if (m) {
310
+ hits.push({ id: pat.id, ruleId: formatSecretRuleId(pat), match: m[0] });
311
+ }
312
+ }
313
+ return hits;
314
+ }
315
+
84
316
  /**
85
317
  * Compile the user-configured blocked-path regex. Falls back to the default
86
318
  * pattern if the configured value is missing or malformed — we never let a
@@ -161,19 +393,18 @@ export function checkHardcodedSecrets(input) {
161
393
  if (candidates.length === 0) return null;
162
394
 
163
395
  for (const text of candidates) {
164
- for (const pat of HARDCODED_SECRET_PATTERNS) {
165
- if (pat.re.test(text)) {
166
- return {
167
- blocked: true,
168
- ruleId: `SONAR-SEC-${pat.id}`,
169
- reason:
170
- `A likely hardcoded secret (${pat.id}) was detected in the proposed content. ` +
171
- `Per the Golden Rule in CLAUDE.md, credentials must never be embedded in source code. ` +
172
- `Corrective action: move the value to an environment variable or a managed secret; ` +
173
- `do not commit tokens, private keys, or JWTs to the source tree under any circumstance.`,
174
- };
175
- }
176
- }
396
+ const hits = findSecretHits(text);
397
+ if (hits.length === 0) continue;
398
+ const hit = /** @type {{ id: string, ruleId: string, match: string }} */ (hits[0]);
399
+ return {
400
+ blocked: true,
401
+ ruleId: hit.ruleId,
402
+ reason:
403
+ `A likely hardcoded secret (${hit.id}) was detected in the proposed content. ` +
404
+ `Per the Golden Rule in CLAUDE.md, credentials must never be embedded in source code. ` +
405
+ `Corrective action: move the value to an environment variable or a managed secret; ` +
406
+ `do not commit tokens, private keys, or JWTs to the source tree under any circumstance.`,
407
+ };
177
408
  }
178
409
  return null;
179
410
  }
@@ -194,21 +425,19 @@ export function checkDestructiveBash(input) {
194
425
  typeof input.tool_input.command === "string" ? input.tool_input.command : undefined;
195
426
  if (!command) return null;
196
427
 
197
- for (const pat of DESTRUCTIVE_BASH_PATTERNS) {
198
- if (pat.re.test(command)) {
199
- return {
200
- blocked: true,
201
- ruleId: `SONAR-BASH-${pat.id}`,
202
- reason:
203
- `The proposed Bash command matched the destructive pattern ${pat.id}: '${command}'. ` +
204
- `claude-crap blocks operations that can wipe the project tree, rewrite published git history, ` +
205
- `or execute remote code without review. ` +
206
- `Corrective action: if this operation is truly intended, ask the user to confirm and run it ` +
207
- `manually from their own terminal instead of through the agent.`,
208
- };
209
- }
210
- }
211
- return null;
428
+ const hit = findDestructiveBashHit(command);
429
+ if (!hit) return null;
430
+
431
+ return {
432
+ blocked: true,
433
+ ruleId: hit.ruleId,
434
+ reason:
435
+ `The proposed Bash command matched the destructive pattern ${hit.id}: '${command}'. ` +
436
+ `claude-crap blocks operations that can wipe the project tree, rewrite published git history, ` +
437
+ `or execute remote code without review. ` +
438
+ `Corrective action: if this operation is truly intended, ask the user to confirm and run it ` +
439
+ `manually from their own terminal instead of through the agent.`,
440
+ };
212
441
  }
213
442
 
214
443
  /**
@@ -150,6 +150,9 @@ const LOC_WALK_SKIP_DIRS = new Set([
150
150
  ".git",
151
151
  // Build outputs
152
152
  "dist", "build", "bundle", "out", "target", "coverage",
153
+ // Test coverage report bundles (ReportGenerator, Istanbul, coverage.py, dotnet test)
154
+ "coverage-report", "CoverageReport", "coveragereport",
155
+ "TestResults", "cobertura", "lcov-report", "htmlcov",
153
156
  // Framework build outputs
154
157
  ".next", ".nuxt", ".output", ".vercel", ".svelte-kit",
155
158
  ".astro", ".angular", ".turbo", ".parcel-cache", ".expo",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-crap-plugin",
9
- "version": "0.4.5",
9
+ "version": "0.4.6",
10
10
  "dependencies": {
11
11
  "@fastify/static": "^8.0.3",
12
12
  "@modelcontextprotocol/sdk": "^1.0.4",
@@ -1208,9 +1208,9 @@
1208
1208
  "license": "BSD-3-Clause"
1209
1209
  },
1210
1210
  "node_modules/fastify": {
1211
- "version": "5.8.4",
1212
- "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz",
1213
- "integrity": "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==",
1211
+ "version": "5.8.5",
1212
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz",
1213
+ "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==",
1214
1214
  "funding": [
1215
1215
  {
1216
1216
  "type": "github",
@@ -1505,9 +1505,9 @@
1505
1505
  }
1506
1506
  },
1507
1507
  "node_modules/hono": {
1508
- "version": "4.12.12",
1509
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
1510
- "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
1508
+ "version": "4.12.14",
1509
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz",
1510
+ "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==",
1511
1511
  "license": "MIT",
1512
1512
  "engines": {
1513
1513
  "node": ">=16.9.0"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
@@ -58,6 +58,12 @@ export interface FileDetailSummary {
58
58
  /** Full response payload for the file detail endpoint. */
59
59
  export interface FileDetailResponse {
60
60
  readonly filePath: string;
61
+ /**
62
+ * Absolute path on the host filesystem, already resolved through the
63
+ * workspace-traversal guard. The UI uses this to build editor
64
+ * deep-links (e.g. `vscode://file/{absolutePath}:{line}`).
65
+ */
66
+ readonly absolutePath: string;
61
67
  readonly language: SupportedLanguage | null;
62
68
  readonly physicalLoc: number;
63
69
  readonly logicalLoc: number;
@@ -175,6 +181,7 @@ export async function buildFileDetail(
175
181
 
176
182
  return {
177
183
  filePath: relativePath,
184
+ absolutePath,
178
185
  language,
179
186
  physicalLoc,
180
187
  logicalLoc,