memorydetective 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/README.md +84 -15
  3. package/USAGE.md +27 -4
  4. package/dist/cli.js +106 -3
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.js +42 -22
  7. package/dist/index.js.map +1 -1
  8. package/dist/parsers/referenceTree.d.ts +111 -0
  9. package/dist/parsers/referenceTree.js +328 -0
  10. package/dist/parsers/referenceTree.js.map +1 -0
  11. package/dist/runtime/axe.js +6 -1
  12. package/dist/runtime/axe.js.map +1 -1
  13. package/dist/runtime/exec.d.ts +30 -0
  14. package/dist/runtime/exec.js +30 -3
  15. package/dist/runtime/exec.js.map +1 -1
  16. package/dist/runtime/fixTemplates.js +67 -0
  17. package/dist/runtime/fixTemplates.js.map +1 -1
  18. package/dist/runtime/leakReport.d.ts +62 -0
  19. package/dist/runtime/leakReport.js +138 -0
  20. package/dist/runtime/leakReport.js.map +1 -0
  21. package/dist/runtime/platformCheck.d.ts +54 -0
  22. package/dist/runtime/platformCheck.js +94 -0
  23. package/dist/runtime/platformCheck.js.map +1 -0
  24. package/dist/runtime/redact.d.ts +66 -0
  25. package/dist/runtime/redact.js +146 -0
  26. package/dist/runtime/redact.js.map +1 -0
  27. package/dist/runtime/responseFormatter.d.ts +78 -0
  28. package/dist/runtime/responseFormatter.js +307 -0
  29. package/dist/runtime/responseFormatter.js.map +1 -0
  30. package/dist/runtime/securityFlags.d.ts +74 -0
  31. package/dist/runtime/securityFlags.js +90 -0
  32. package/dist/runtime/securityFlags.js.map +1 -0
  33. package/dist/runtime/staticAnalysisHints.js +14 -1
  34. package/dist/runtime/staticAnalysisHints.js.map +1 -1
  35. package/dist/templates/leak-report.html +39 -0
  36. package/dist/templates/templates/leak-report.html +39 -0
  37. package/dist/tools/analyzeAbandonedMemory.d.ts +162 -0
  38. package/dist/tools/analyzeAbandonedMemory.js +325 -0
  39. package/dist/tools/analyzeAbandonedMemory.js.map +1 -0
  40. package/dist/tools/analyzeAllocations.d.ts +11 -2
  41. package/dist/tools/analyzeAllocations.js +4 -0
  42. package/dist/tools/analyzeAllocations.js.map +1 -1
  43. package/dist/tools/analyzeAnimationHitches.d.ts +32 -2
  44. package/dist/tools/analyzeAnimationHitches.js +25 -4
  45. package/dist/tools/analyzeAnimationHitches.js.map +1 -1
  46. package/dist/tools/analyzeAppLaunch.d.ts +3 -0
  47. package/dist/tools/analyzeAppLaunch.js +2 -0
  48. package/dist/tools/analyzeAppLaunch.js.map +1 -1
  49. package/dist/tools/analyzeHangs.d.ts +78 -2
  50. package/dist/tools/analyzeHangs.js +117 -4
  51. package/dist/tools/analyzeHangs.js.map +1 -1
  52. package/dist/tools/analyzeMemgraph.d.ts +40 -1
  53. package/dist/tools/analyzeMemgraph.js +66 -2
  54. package/dist/tools/analyzeMemgraph.js.map +1 -1
  55. package/dist/tools/analyzeTimeProfile.d.ts +11 -1
  56. package/dist/tools/analyzeTimeProfile.js +5 -0
  57. package/dist/tools/analyzeTimeProfile.js.map +1 -1
  58. package/dist/tools/bootAndLaunchForLeakInvestigation.d.ts +18 -9
  59. package/dist/tools/bootAndLaunchForLeakInvestigation.js +27 -0
  60. package/dist/tools/bootAndLaunchForLeakInvestigation.js.map +1 -1
  61. package/dist/tools/captureMemgraph.d.ts +22 -4
  62. package/dist/tools/captureMemgraph.js +42 -9
  63. package/dist/tools/captureMemgraph.js.map +1 -1
  64. package/dist/tools/captureScenarioState.d.ts +12 -4
  65. package/dist/tools/captureScenarioState.js +4 -0
  66. package/dist/tools/captureScenarioState.js.map +1 -1
  67. package/dist/tools/classifyCycle.js +77 -0
  68. package/dist/tools/classifyCycle.js.map +1 -1
  69. package/dist/tools/cleanupTraces.d.ts +87 -0
  70. package/dist/tools/cleanupTraces.js +232 -0
  71. package/dist/tools/cleanupTraces.js.map +1 -0
  72. package/dist/tools/compareTracesByPattern.d.ts +2 -2
  73. package/dist/tools/detectLeaksInXCTest.d.ts +116 -0
  74. package/dist/tools/detectLeaksInXCTest.js +311 -0
  75. package/dist/tools/detectLeaksInXCTest.js.map +1 -0
  76. package/dist/tools/detectLeaksInXCUITest.d.ts +8 -3
  77. package/dist/tools/detectLeaksInXCUITest.js +30 -4
  78. package/dist/tools/detectLeaksInXCUITest.js.map +1 -1
  79. package/dist/tools/diffMemgraphs.d.ts +5 -2
  80. package/dist/tools/diffMemgraphs.js +2 -0
  81. package/dist/tools/diffMemgraphs.js.map +1 -1
  82. package/dist/tools/findCycles.d.ts +1 -1
  83. package/dist/tools/recordTimeProfile.d.ts +29 -9
  84. package/dist/tools/recordTimeProfile.js +70 -7
  85. package/dist/tools/recordTimeProfile.js.map +1 -1
  86. package/dist/tools/renderCycleGraph.d.ts +1 -1
  87. package/dist/tools/verifyFix.d.ts +2 -2
  88. package/dist/types.d.ts +24 -1
  89. package/package.json +3 -3
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Self-contained HTML rendering for `detectLeaksInXCTest` and
3
+ * `detectLeaksInXCUITest`.
4
+ *
5
+ * The rendered file embeds inline CSS (no external assets) so it renders the
6
+ * same way in:
7
+ *
8
+ * - GitHub's file preview after upload-artifact + download
9
+ * - PR-comment bots that link to the artifact URL
10
+ * - A local `open report.html` from a CI failure investigation
11
+ *
12
+ * The template lives at `src/templates/leak-report.html` and is substituted
13
+ * with three placeholders: `{{TITLE}}`, `{{VERSION}}`, `{{TIMESTAMP}}`,
14
+ * `{{BODY}}`. The body fragment is generated per-call and represents the
15
+ * per-test sections (totals, new-cycles table, embedded steps log).
16
+ *
17
+ * Inputs are intentionally narrowed to the result shapes the two detect
18
+ * tools share, so the renderer can be called from either tool without
19
+ * conditional branches.
20
+ */
21
+ export interface LeakReportSection {
22
+ /** Short label that goes at the top of the section (e.g. test identifier). */
23
+ title: string;
24
+ /** True when the section passed all checks (green verdict pill). */
25
+ passed: boolean;
26
+ /** Free-text reason rendered next to the pill when `passed` is false. */
27
+ failureReason?: string;
28
+ /** Absolute path to the baseline `.memgraph` (rendered as a `<code>` line). */
29
+ baselineMemgraph?: string;
30
+ /** Absolute path to the after `.memgraph`. */
31
+ afterMemgraph?: string;
32
+ totals: {
33
+ baselineLeaks: number;
34
+ afterLeaks: number;
35
+ leakDelta: number;
36
+ };
37
+ newCycles: Array<{
38
+ rootClass: string;
39
+ chainLength: number;
40
+ allowlisted: boolean;
41
+ }>;
42
+ /** Step-by-step log, rendered inside a collapsed `<details>` block. */
43
+ steps?: string[];
44
+ }
45
+ export interface RenderLeakReportInput {
46
+ title: string;
47
+ /** Optional context line (e.g. scheme, destination) rendered under the H1. */
48
+ subtitle?: string;
49
+ sections: LeakReportSection[];
50
+ }
51
+ export declare function renderLeakReportHtml(input: RenderLeakReportInput): string;
52
+ /**
53
+ * Render and persist the HTML report. Returns the absolute path it was
54
+ * written to. The caller is responsible for choosing the path (so the same
55
+ * helper drives both the XCTest and XCUITest tools without each tool having
56
+ * to know about path conventions).
57
+ */
58
+ export declare function writeLeakReportHtml(outputPath: string, input: RenderLeakReportInput): string;
59
+ export declare function _resetTemplateCacheForTests(): void;
60
+ export declare function _templatePathForTests(): string;
61
+ export declare const LEAK_REPORT_TEMPLATE_PATH: string;
62
+ export type { LeakReportSection as LeakReportSectionType };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Self-contained HTML rendering for `detectLeaksInXCTest` and
3
+ * `detectLeaksInXCUITest`.
4
+ *
5
+ * The rendered file embeds inline CSS (no external assets) so it renders the
6
+ * same way in:
7
+ *
8
+ * - GitHub's file preview after upload-artifact + download
9
+ * - PR-comment bots that link to the artifact URL
10
+ * - A local `open report.html` from a CI failure investigation
11
+ *
12
+ * The template lives at `src/templates/leak-report.html` and is substituted
13
+ * with three placeholders: `{{TITLE}}`, `{{VERSION}}`, `{{TIMESTAMP}}`,
14
+ * `{{BODY}}`. The body fragment is generated per-call and represents the
15
+ * per-test sections (totals, new-cycles table, embedded steps log).
16
+ *
17
+ * Inputs are intentionally narrowed to the result shapes the two detect
18
+ * tools share, so the renderer can be called from either tool without
19
+ * conditional branches.
20
+ */
21
+ import { readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, resolve as resolvePath } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { VERSION } from "../version.js";
25
+ const here = dirname(fileURLToPath(import.meta.url));
26
+ // `dist/runtime/leakReport.js` -> `dist/templates/leak-report.html`
27
+ // (matches the `tsconfig.json` layout where src and dist are parallel).
28
+ const TEMPLATE_PATH = resolvePath(here, "..", "templates", "leak-report.html");
29
+ let cachedTemplate = null;
30
+ function loadTemplate() {
31
+ if (cachedTemplate == null) {
32
+ cachedTemplate = readFileSync(TEMPLATE_PATH, "utf8");
33
+ }
34
+ return cachedTemplate;
35
+ }
36
+ function escapeHtml(s) {
37
+ return s
38
+ .replaceAll("&", "&amp;")
39
+ .replaceAll("<", "&lt;")
40
+ .replaceAll(">", "&gt;")
41
+ .replaceAll('"', "&quot;")
42
+ .replaceAll("'", "&#39;");
43
+ }
44
+ function renderSection(section) {
45
+ const verdict = section.passed
46
+ ? '<span class="verdict pass">PASS</span>'
47
+ : '<span class="verdict fail">FAIL</span>';
48
+ const failReason = section.failureReason
49
+ ? `<p style="margin-top:.5rem;color:#ff3b30">${escapeHtml(section.failureReason)}</p>`
50
+ : "";
51
+ const baseline = section.baselineMemgraph
52
+ ? `<div><span class="label">Baseline:</span> <code>${escapeHtml(section.baselineMemgraph)}</code></div>`
53
+ : "";
54
+ const after = section.afterMemgraph
55
+ ? `<div><span class="label">After:</span> <code>${escapeHtml(section.afterMemgraph)}</code></div>`
56
+ : "";
57
+ const stats = `
58
+ <div style="margin:.75rem 0">
59
+ <div class="stat"><div class="n">${section.totals.baselineLeaks}</div><div class="label">Baseline</div></div>
60
+ <div class="stat"><div class="n">${section.totals.afterLeaks}</div><div class="label">After</div></div>
61
+ <div class="stat"><div class="n">${section.totals.leakDelta >= 0 ? "+" : ""}${section.totals.leakDelta}</div><div class="label">Delta</div></div>
62
+ </div>`;
63
+ const cyclesTable = section.newCycles.length === 0
64
+ ? '<p style="opacity:.65;margin:.5rem 0">No new ROOT CYCLEs after the test.</p>'
65
+ : `
66
+ <table>
67
+ <thead><tr><th>Root class</th><th>Chain length</th><th>Status</th></tr></thead>
68
+ <tbody>${section.newCycles
69
+ .map((c) => `
70
+ <tr>
71
+ <td><code>${escapeHtml(c.rootClass)}</code></td>
72
+ <td>${c.chainLength}</td>
73
+ <td>${c.allowlisted
74
+ ? '<span class="badge allow">allowlisted</span>'
75
+ : '<span class="badge fail">new leak</span>'}</td>
76
+ </tr>`)
77
+ .join("")}
78
+ </tbody>
79
+ </table>`;
80
+ const stepsBlock = section.steps && section.steps.length > 0
81
+ ? `<details><summary>Run log (${section.steps.length} step${section.steps.length === 1 ? "" : "s"})</summary><div class="steps">${section.steps.map(escapeHtml).join("\n")}</div></details>`
82
+ : "";
83
+ return `
84
+ <section class="card">
85
+ <h2 style="margin:0">${escapeHtml(section.title)} ${verdict}</h2>
86
+ ${failReason}
87
+ ${baseline}
88
+ ${after}
89
+ ${stats}
90
+ ${cyclesTable}
91
+ ${stepsBlock}
92
+ </section>`;
93
+ }
94
+ export function renderLeakReportHtml(input) {
95
+ const template = loadTemplate();
96
+ const body = (input.subtitle
97
+ ? `<div class="meta" style="margin-top:-1rem;margin-bottom:1.25rem">${escapeHtml(input.subtitle)}</div>`
98
+ : "") + input.sections.map(renderSection).join("\n");
99
+ return template
100
+ .replace(/\{\{TITLE\}\}/g, escapeHtml(input.title))
101
+ .replace(/\{\{VERSION\}\}/g, escapeHtml(VERSION))
102
+ .replace(/\{\{TIMESTAMP\}\}/g, escapeHtml(new Date().toISOString()))
103
+ .replace(/\{\{BODY\}\}/g, body);
104
+ }
105
+ /**
106
+ * Render and persist the HTML report. Returns the absolute path it was
107
+ * written to. The caller is responsible for choosing the path (so the same
108
+ * helper drives both the XCTest and XCUITest tools without each tool having
109
+ * to know about path conventions).
110
+ */
111
+ export function writeLeakReportHtml(outputPath, input) {
112
+ const absolute = resolvePath(outputPath);
113
+ writeFileSync(absolute, renderLeakReportHtml(input), "utf8");
114
+ return absolute;
115
+ }
116
+ // Exposed for tests so the cached template can be reset between cases.
117
+ export function _resetTemplateCacheForTests() {
118
+ cachedTemplate = null;
119
+ }
120
+ export function _templatePathForTests() {
121
+ return TEMPLATE_PATH;
122
+ }
123
+ // Build-time pin so `npm pack` ships the template alongside `dist/`. The
124
+ // `package.json` `files` field needs to include `src/templates/**` AND
125
+ // `dist/templates/**` after the build copies them. Verified in tests by
126
+ // reading `loadTemplate()` synchronously: if the template is missing, the
127
+ // first leak-report render throws an obvious ENOENT.
128
+ //
129
+ // Why pin the path instead of bundling the HTML as a string constant?
130
+ // Keeping the template as a real HTML file lets contributors preview it in
131
+ // a browser without going through a TS rebuild, and keeps the CSS-heavy
132
+ // section out of TypeScript's diagnostics.
133
+ const _BUILD_NOTE = "see comment block";
134
+ void _BUILD_NOTE;
135
+ // Re-export so other build steps can locate the template path without
136
+ // importing the private helper.
137
+ export const LEAK_REPORT_TEMPLATE_PATH = TEMPLATE_PATH;
138
+ //# sourceMappingURL=leakReport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leakReport.js","sourceRoot":"","sources":["../../src/runtime/leakReport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Platform-specific advisories surfaced before capture-class tool calls.
3
+ *
4
+ * Today this only handles the macOS 26.x `task_for_pid` regression that
5
+ * blocks `leaks --outputGraph`, `heap`, and `xctrace --template Allocations`
6
+ * against iOS simulator processes regardless of `MallocStackLogging`.
7
+ * Surfaced from the notelet investigation 2026-05-12, where three CLI
8
+ * memory-introspection paths all failed with `Failed to get DYLD info for
9
+ * task` / minimal-corpse, and even Xcode's "View Memory Graph Hierarchy"
10
+ * failed unless Malloc Stack Logging was enabled in the scheme.
11
+ *
12
+ * The advisory is informational and idempotent: emitted once per server
13
+ * instance via {@link maybeLogPlatformAdvisoryOnce} (stderr), and returned
14
+ * as a structured `platformAdvisory` field on capture-class tool responses
15
+ * via {@link getPlatformAdvisory} so agents can surface it to the user
16
+ * before any tool work.
17
+ */
18
+ export type PlatformAdvisory = {
19
+ issue: "macos-26-task-for-pid-broken";
20
+ message: string;
21
+ recommendedActions: string[];
22
+ };
23
+ /**
24
+ * Pure: returns the structured platform advisory for the current host,
25
+ * or `null` when no advisory applies.
26
+ *
27
+ * The advisory is suppressed when:
28
+ * - Host is not macOS.
29
+ * - Host is macOS but not in the 26.x range (Darwin kernel 25.x).
30
+ * - `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY=1` is set in the environment.
31
+ *
32
+ * Conservative: when the Darwin major cannot be parsed, returns `null`
33
+ * (no advisory) rather than emitting a false positive. The macOS 27.x case
34
+ * (Darwin 26.x) is also `null` pending verification of whether Apple
35
+ * shipped a kernel fix; reopen this helper when 27.x lands.
36
+ *
37
+ * @param env - Environment lookup (defaults to `process.env`).
38
+ * Threaded as a parameter for testability.
39
+ * @param osPlatform - `os.platform()` value (defaults to live).
40
+ * @param osRelease - `os.release()` value (defaults to live).
41
+ */
42
+ export declare function getPlatformAdvisory(env?: Readonly<Record<string, string | undefined>>, osPlatform?: () => NodeJS.Platform, osRelease?: () => string): PlatformAdvisory | null;
43
+ /**
44
+ * Side-effecting: emits the platform advisory to stderr on the FIRST call
45
+ * per server instance, then no-ops on subsequent calls. Safe to call at
46
+ * the top of every capture-class tool.
47
+ *
48
+ * The once-per-instance flag is module-level so it survives across tool
49
+ * calls within the same server process. Tests can reset via
50
+ * {@link resetPlatformAdvisoryFlagForTests}.
51
+ */
52
+ export declare function maybeLogPlatformAdvisoryOnce(advisory: PlatformAdvisory | null, writer?: (line: string) => void): void;
53
+ /** Test-only: reset the once-per-instance flag. */
54
+ export declare function resetPlatformAdvisoryFlagForTests(): void;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Platform-specific advisories surfaced before capture-class tool calls.
3
+ *
4
+ * Today this only handles the macOS 26.x `task_for_pid` regression that
5
+ * blocks `leaks --outputGraph`, `heap`, and `xctrace --template Allocations`
6
+ * against iOS simulator processes regardless of `MallocStackLogging`.
7
+ * Surfaced from the notelet investigation 2026-05-12, where three CLI
8
+ * memory-introspection paths all failed with `Failed to get DYLD info for
9
+ * task` / minimal-corpse, and even Xcode's "View Memory Graph Hierarchy"
10
+ * failed unless Malloc Stack Logging was enabled in the scheme.
11
+ *
12
+ * The advisory is informational and idempotent: emitted once per server
13
+ * instance via {@link maybeLogPlatformAdvisoryOnce} (stderr), and returned
14
+ * as a structured `platformAdvisory` field on capture-class tool responses
15
+ * via {@link getPlatformAdvisory} so agents can surface it to the user
16
+ * before any tool work.
17
+ */
18
+ import os from "node:os";
19
+ const ADVISORY_MESSAGE = "macOS 26.x has an Apple-side kernel regression in `task_for_pid` against simulator processes. " +
20
+ "`leaks --outputGraph`, `heap`, and `xctrace --template Allocations` all abort with " +
21
+ "`Failed to get DYLD info for task` / minimal-corpse, even with `MallocStackLogging=1` " +
22
+ "applied at launch. memorydetective surfaces this via `workaroundNotice` when capture " +
23
+ "is attempted. The most reliable workaround today is to use an iOS 18 simulator runtime, " +
24
+ "which is pre-regression. Set `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY=1` to silence " +
25
+ "this notice.";
26
+ const ADVISORY_ACTIONS = [
27
+ "Install an iOS 18 simulator runtime via Xcode > Settings > Platforms > +iOS 18.x.",
28
+ "When capturing a `.memgraph`, target the iOS 18 simulator rather than a macOS 26.x sim.",
29
+ "If iOS 18 is not feasible, fall back to manual Xcode `Debug > View Memory Graph Hierarchy` with Malloc Stack Logging enabled in the scheme's Diagnostics tab.",
30
+ "Set `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY=1` in the environment to silence this notice.",
31
+ ];
32
+ /**
33
+ * Pure: returns the structured platform advisory for the current host,
34
+ * or `null` when no advisory applies.
35
+ *
36
+ * The advisory is suppressed when:
37
+ * - Host is not macOS.
38
+ * - Host is macOS but not in the 26.x range (Darwin kernel 25.x).
39
+ * - `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY=1` is set in the environment.
40
+ *
41
+ * Conservative: when the Darwin major cannot be parsed, returns `null`
42
+ * (no advisory) rather than emitting a false positive. The macOS 27.x case
43
+ * (Darwin 26.x) is also `null` pending verification of whether Apple
44
+ * shipped a kernel fix; reopen this helper when 27.x lands.
45
+ *
46
+ * @param env - Environment lookup (defaults to `process.env`).
47
+ * Threaded as a parameter for testability.
48
+ * @param osPlatform - `os.platform()` value (defaults to live).
49
+ * @param osRelease - `os.release()` value (defaults to live).
50
+ */
51
+ export function getPlatformAdvisory(env = process.env, osPlatform = os.platform, osRelease = os.release) {
52
+ if (env.MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY === "1")
53
+ return null;
54
+ if (osPlatform() !== "darwin")
55
+ return null;
56
+ const release = osRelease();
57
+ const majorStr = release.split(".")[0];
58
+ const major = Number.parseInt(majorStr, 10);
59
+ if (!Number.isFinite(major))
60
+ return null;
61
+ // Darwin 25.x kernel ships with macOS 26.x. Darwin 24.x = macOS 25 / Sequoia
62
+ // (no regression). Darwin 26.x = macOS 27 (verification pending; no advisory
63
+ // until confirmed).
64
+ if (major !== 25)
65
+ return null;
66
+ return {
67
+ issue: "macos-26-task-for-pid-broken",
68
+ message: ADVISORY_MESSAGE,
69
+ recommendedActions: ADVISORY_ACTIONS,
70
+ };
71
+ }
72
+ let advisoryLoggedThisInstance = false;
73
+ /**
74
+ * Side-effecting: emits the platform advisory to stderr on the FIRST call
75
+ * per server instance, then no-ops on subsequent calls. Safe to call at
76
+ * the top of every capture-class tool.
77
+ *
78
+ * The once-per-instance flag is module-level so it survives across tool
79
+ * calls within the same server process. Tests can reset via
80
+ * {@link resetPlatformAdvisoryFlagForTests}.
81
+ */
82
+ export function maybeLogPlatformAdvisoryOnce(advisory, writer = (line) => process.stderr.write(line)) {
83
+ if (advisoryLoggedThisInstance)
84
+ return;
85
+ if (advisory == null)
86
+ return;
87
+ writer(`[memorydetective] platform advisory: ${advisory.message}\n`);
88
+ advisoryLoggedThisInstance = true;
89
+ }
90
+ /** Test-only: reset the once-per-instance flag. */
91
+ export function resetPlatformAdvisoryFlagForTests() {
92
+ advisoryLoggedThisInstance = false;
93
+ }
94
+ //# sourceMappingURL=platformCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platformCheck.js","sourceRoot":"","sources":["../../src/runtime/platformCheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AAQzB,MAAM,gBAAgB,GACpB,gGAAgG;IAChG,qFAAqF;IACrF,wFAAwF;IACxF,uFAAuF;IACvF,0FAA0F;IAC1F,yFAAyF;IACzF,cAAc,CAAC;AAEjB,MAAM,gBAAgB,GAAG;IACvB,mFAAmF;IACnF,yFAAyF;IACzF,+JAA+J;IAC/J,+FAA+F;CAChG,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAoD,OAAO,CAAC,GAAG,EAC/D,aAAoC,EAAE,CAAC,QAAQ,EAC/C,YAA0B,EAAE,CAAC,OAAO;IAEpC,IAAI,GAAG,CAAC,0CAA0C,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACxE,IAAI,UAAU,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,SAAS,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,6EAA6E;IAC7E,6EAA6E;IAC7E,oBAAoB;IACpB,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC9B,OAAO;QACL,KAAK,EAAE,8BAA8B;QACrC,OAAO,EAAE,gBAAgB;QACzB,kBAAkB,EAAE,gBAAgB;KACrC,CAAC;AACJ,CAAC;AAED,IAAI,0BAA0B,GAAG,KAAK,CAAC;AAEvC;;;;;;;;GAQG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAiC,EACjC,SAAiC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;IAErE,IAAI,0BAA0B;QAAE,OAAO;IACvC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO;IAC7B,MAAM,CAAC,wCAAwC,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;IACrE,0BAA0B,GAAG,IAAI,CAAC;AACpC,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,iCAAiC;IAC/C,0BAA0B,GAAG,KAAK,CAAC;AACrC,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Redact sensitive substrings from tool responses before they leave the
3
+ * MCP boundary.
4
+ *
5
+ * Tool outputs frequently include filesystem paths (with the user's home
6
+ * directory in them), token-shaped strings, hostnames, IP addresses, and
7
+ * bundle identifiers. None of those are useful to an AI agent reasoning
8
+ * about an iOS leak, and all of them are footguns when the output ends
9
+ * up in a Slack message, a PR comment, or a screenshot. This module
10
+ * scrubs them at the formatter boundary so by-default outputs are safe
11
+ * to share without manual sweeping.
12
+ *
13
+ * Three modes are selected via the `MEMORYDETECTIVE_REDACTION` env var:
14
+ *
15
+ * - `balanced` (default): home-directory absolute paths become `~/...`,
16
+ * common secret-shaped tokens (AWS keys, GitHub PATs, Stripe secrets,
17
+ * Slack tokens, Bearer auth) are masked. Hostnames, IPs, bundle IDs,
18
+ * process names, and class names are preserved (they are usually
19
+ * useful for debugging).
20
+ *
21
+ * - `strict`: everything in `balanced`, plus hostnames, IPv4 addresses,
22
+ * and bundle identifiers. Use when the output is going to be pasted
23
+ * into a public artifact (issue tracker, blog post, social) and you
24
+ * want a wide safety margin.
25
+ *
26
+ * - `off`: no redaction. Default behavior is preserved for legacy
27
+ * workflows and for local-only debugging where the noise is genuinely
28
+ * helpful. The startup banner logs the active mode so an operator
29
+ * running `off` knows the responses are unfiltered.
30
+ *
31
+ * Redaction is structural: the value passed in keeps its shape (same
32
+ * object keys, same array lengths, same scalar types), only string
33
+ * leaves are rewritten. Numbers, booleans, null/undefined, and dates
34
+ * pass through unchanged.
35
+ */
36
+ export type RedactionMode = "balanced" | "strict" | "off";
37
+ /**
38
+ * Pure: read the active redaction mode from an env-like object.
39
+ * Defaults to `balanced` when unset or set to an unrecognized value.
40
+ *
41
+ * Threaded as a parameter for testability; production callers omit it
42
+ * and get `process.env`.
43
+ */
44
+ export declare function getRedactionMode(env?: Readonly<Record<string, string | undefined>>): RedactionMode;
45
+ /**
46
+ * Pure: scrub a single string per the active mode. `off` returns the
47
+ * input unchanged; the other modes apply the rules described in the
48
+ * module doc.
49
+ *
50
+ * Threaded `homeDir` for testability so unit tests can pass a fake
51
+ * `/Users/test/` without depending on the real home directory.
52
+ */
53
+ export declare function redactString(input: string, mode: RedactionMode, homeDir?: string): string;
54
+ /**
55
+ * Pure: recursively redact strings inside an arbitrary JSON-shaped
56
+ * value. Object key names are preserved unchanged (they are part of
57
+ * the schema, not data); only string VALUES and string ARRAY items
58
+ * are scrubbed.
59
+ *
60
+ * Non-string scalars (number, boolean, null) pass through. Functions,
61
+ * symbols, and other non-JSON values are returned as-is.
62
+ */
63
+ export declare function redact(value: unknown, mode: RedactionMode, homeDir?: string): unknown;
64
+ export declare function maybeLogRedactionModeOnce(mode: RedactionMode, writer?: (line: string) => void): void;
65
+ /** Test-only: reset the once-per-instance log flag. */
66
+ export declare function resetRedactionAdvisoryFlagForTests(): void;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Redact sensitive substrings from tool responses before they leave the
3
+ * MCP boundary.
4
+ *
5
+ * Tool outputs frequently include filesystem paths (with the user's home
6
+ * directory in them), token-shaped strings, hostnames, IP addresses, and
7
+ * bundle identifiers. None of those are useful to an AI agent reasoning
8
+ * about an iOS leak, and all of them are footguns when the output ends
9
+ * up in a Slack message, a PR comment, or a screenshot. This module
10
+ * scrubs them at the formatter boundary so by-default outputs are safe
11
+ * to share without manual sweeping.
12
+ *
13
+ * Three modes are selected via the `MEMORYDETECTIVE_REDACTION` env var:
14
+ *
15
+ * - `balanced` (default): home-directory absolute paths become `~/...`,
16
+ * common secret-shaped tokens (AWS keys, GitHub PATs, Stripe secrets,
17
+ * Slack tokens, Bearer auth) are masked. Hostnames, IPs, bundle IDs,
18
+ * process names, and class names are preserved (they are usually
19
+ * useful for debugging).
20
+ *
21
+ * - `strict`: everything in `balanced`, plus hostnames, IPv4 addresses,
22
+ * and bundle identifiers. Use when the output is going to be pasted
23
+ * into a public artifact (issue tracker, blog post, social) and you
24
+ * want a wide safety margin.
25
+ *
26
+ * - `off`: no redaction. Default behavior is preserved for legacy
27
+ * workflows and for local-only debugging where the noise is genuinely
28
+ * helpful. The startup banner logs the active mode so an operator
29
+ * running `off` knows the responses are unfiltered.
30
+ *
31
+ * Redaction is structural: the value passed in keeps its shape (same
32
+ * object keys, same array lengths, same scalar types), only string
33
+ * leaves are rewritten. Numbers, booleans, null/undefined, and dates
34
+ * pass through unchanged.
35
+ */
36
+ import os from "node:os";
37
+ const TOKEN_PATTERNS = [
38
+ // AWS access key id (AKIA + 16 chars)
39
+ { re: /\bAKIA[0-9A-Z]{16}\b/g, keepPrefix: 4 },
40
+ // GitHub classic PAT
41
+ { re: /\bghp_[A-Za-z0-9]{36,}\b/g, keepPrefix: 4 },
42
+ // GitHub fine-grained PAT
43
+ { re: /\bgithub_pat_[A-Za-z0-9_]+\b/g, keepPrefix: 11 },
44
+ // Stripe live + test secret
45
+ { re: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g, keepPrefix: 8 },
46
+ // Slack tokens
47
+ { re: /\bxox[bpoasr]-[A-Za-z0-9-]+\b/g, keepPrefix: 5 },
48
+ // Bearer auth tokens
49
+ { re: /\bBearer\s+[A-Za-z0-9._\-]{20,}\b/gi, keepPrefix: 7 },
50
+ ];
51
+ // Hostnames: 1+ labels followed by a TLD (2+ alpha chars). Excludes the
52
+ // .swift / .ts / .js / .json / .trace / .memgraph "extensions" we see in
53
+ // paths, since those would false-positive otherwise.
54
+ const HOST_PATTERN = /\b(?!(?:[A-Za-z0-9-]+)\.(?:swift|ts|js|json|trace|memgraph|md|yaml|yml|html|png|jpg|jpeg|gif|m4v|mp4|mov|css|sh|py|rb|go|rs|c|cpp|h|hpp|m|mm|plist|xcframework|xcconfig|xcassets)\b)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
55
+ const IP_PATTERN = /\b(?:\d{1,3}\.){3}\d{1,3}\b/g;
56
+ const BUNDLE_ID_PATTERN = /\bcom\.[a-z0-9_-]+(?:\.[a-z0-9_-]+)+\b/gi;
57
+ /**
58
+ * Pure: read the active redaction mode from an env-like object.
59
+ * Defaults to `balanced` when unset or set to an unrecognized value.
60
+ *
61
+ * Threaded as a parameter for testability; production callers omit it
62
+ * and get `process.env`.
63
+ */
64
+ export function getRedactionMode(env = process.env) {
65
+ const raw = (env.MEMORYDETECTIVE_REDACTION ?? "balanced").toLowerCase();
66
+ if (raw === "off" || raw === "strict" || raw === "balanced") {
67
+ return raw;
68
+ }
69
+ return "balanced";
70
+ }
71
+ /**
72
+ * Pure: scrub a single string per the active mode. `off` returns the
73
+ * input unchanged; the other modes apply the rules described in the
74
+ * module doc.
75
+ *
76
+ * Threaded `homeDir` for testability so unit tests can pass a fake
77
+ * `/Users/test/` without depending on the real home directory.
78
+ */
79
+ export function redactString(input, mode, homeDir = os.homedir()) {
80
+ if (mode === "off")
81
+ return input;
82
+ let result = input;
83
+ // Home dir collapse first so subsequent host/IP rules don't mistakenly
84
+ // grab parts of paths.
85
+ if (homeDir && homeDir.length > 1 && result.includes(homeDir)) {
86
+ result = result.split(homeDir).join("~");
87
+ }
88
+ // Always mask token-shaped secrets, even in `balanced`.
89
+ for (const { re, keepPrefix } of TOKEN_PATTERNS) {
90
+ result = result.replace(re, (match) => match.slice(0, keepPrefix) + "***REDACTED***");
91
+ }
92
+ if (mode === "strict") {
93
+ result = result.replace(BUNDLE_ID_PATTERN, "***BUNDLE_ID***");
94
+ result = result.replace(IP_PATTERN, "***IP***");
95
+ result = result.replace(HOST_PATTERN, "***HOST***");
96
+ }
97
+ return result;
98
+ }
99
+ /**
100
+ * Pure: recursively redact strings inside an arbitrary JSON-shaped
101
+ * value. Object key names are preserved unchanged (they are part of
102
+ * the schema, not data); only string VALUES and string ARRAY items
103
+ * are scrubbed.
104
+ *
105
+ * Non-string scalars (number, boolean, null) pass through. Functions,
106
+ * symbols, and other non-JSON values are returned as-is.
107
+ */
108
+ export function redact(value, mode, homeDir = os.homedir()) {
109
+ if (mode === "off")
110
+ return value;
111
+ if (value == null)
112
+ return value;
113
+ if (typeof value === "string")
114
+ return redactString(value, mode, homeDir);
115
+ if (typeof value === "number" || typeof value === "boolean")
116
+ return value;
117
+ if (Array.isArray(value)) {
118
+ return value.map((v) => redact(v, mode, homeDir));
119
+ }
120
+ if (typeof value === "object") {
121
+ const out = {};
122
+ for (const [k, v] of Object.entries(value)) {
123
+ out[k] = redact(v, mode, homeDir);
124
+ }
125
+ return out;
126
+ }
127
+ return value;
128
+ }
129
+ /**
130
+ * Side-effecting: log the active redaction mode once per server
131
+ * startup so an operator running with `off` knows the responses
132
+ * are unfiltered.
133
+ */
134
+ let advisoryLogged = false;
135
+ export function maybeLogRedactionModeOnce(mode, writer = (line) => process.stderr.write(line)) {
136
+ if (advisoryLogged)
137
+ return;
138
+ writer(`[memorydetective] redaction mode: ${mode}. ` +
139
+ `Set MEMORYDETECTIVE_REDACTION to balanced (default), strict, or off.\n`);
140
+ advisoryLogged = true;
141
+ }
142
+ /** Test-only: reset the once-per-instance log flag. */
143
+ export function resetRedactionAdvisoryFlagForTests() {
144
+ advisoryLogged = false;
145
+ }
146
+ //# sourceMappingURL=redact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/runtime/redact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared response formatter for MCP tool outputs.
3
+ *
4
+ * Tools can declare `outputFormat?: "markdown" | "json" | "both"` (defaults to
5
+ * `"json"`, preserving v1.8 behavior). When the caller asks for a different
6
+ * shape, the registration wrapper in `src/index.ts` calls
7
+ * {@link formatMcpResponse} to produce the actual response content.
8
+ *
9
+ * Two audiences are served at once:
10
+ * - **AI agents** (the typical caller) want raw JSON they can parse and chain
11
+ * into the next call without re-reasoning.
12
+ * - **Humans reading the same response** (the typical second audience: the
13
+ * dev pasting the result into a PR comment, a Slack thread, or a Jira
14
+ * ticket) want a markdown view that highlights the key fields.
15
+ *
16
+ * `outputFormat: "both"` returns BOTH content items in a single response, so a
17
+ * client can display the markdown to the user AND parse the JSON for the
18
+ * agent loop without an extra round-trip.
19
+ */
20
+ import { z } from "zod";
21
+ export declare const outputFormatField: z.ZodOptional<z.ZodEnum<["markdown", "json", "both", "verify-fix-table"]>>;
22
+ export type OutputFormat = z.infer<typeof outputFormatField>;
23
+ export interface McpContentItem {
24
+ type: "text";
25
+ text: string;
26
+ }
27
+ /**
28
+ * Shape of the MCP tool response. Matches the SDK's expected type
29
+ * (which has an open index signature for arbitrary extension fields like
30
+ * `_meta`); we model it explicitly here so the formatter's return type
31
+ * can flow through `server.registerTool` without a cast.
32
+ */
33
+ export interface McpResponse {
34
+ content: McpContentItem[];
35
+ [key: string]: unknown;
36
+ }
37
+ /**
38
+ * Pure: shape the MCP response based on the caller's `outputFormat`.
39
+ *
40
+ * For `json` and `both`, the JSON is `JSON.stringify(result, null, 2)`. For
41
+ * `markdown` and `both`, the markdown is rendered via {@link renderAsMarkdown}.
42
+ */
43
+ export declare function formatMcpResponse(result: unknown, toolName: string, format: OutputFormat | undefined): McpResponse;
44
+ /**
45
+ * Pure: render a verify-fix focused markdown table for tools that support
46
+ * it. Returns `null` if the tool's result does not match the expected
47
+ * verify-fix shape, signaling the caller to fall back to standard markdown.
48
+ *
49
+ * Supported tools:
50
+ *
51
+ * - `analyzeAbandonedMemory`: reads `actionableShrinkage[]` (the v1.10
52
+ * verify-fix-default direction: classes that the fix freed) and
53
+ * `actionableGrowth[]` (regressions the fix didn't address). Emits one
54
+ * table for shrinkage and, when non-empty, a second smaller table for
55
+ * growth. Threshold: |delta| >= 10 by default to filter cosmetic noise.
56
+ *
57
+ * - `diffMemgraphs`: reads `classCountChanges[]` (positive + negative).
58
+ * Future expansion; for now returns null and falls back to standard
59
+ * markdown.
60
+ *
61
+ * The 4-column layout is deliberately compact (Class | Before | After |
62
+ * Delta) so it renders cleanly in GitHub's markdown preview, dev.to, and
63
+ * agent chat contexts. A trailing `> Diagnosis: ...` blockquote carries
64
+ * the structured `diagnosis` field when present.
65
+ */
66
+ export declare function renderVerifyFixTable(result: unknown, toolName: string): string | null;
67
+ /**
68
+ * Pure: render an arbitrary JSON-shaped value as markdown.
69
+ *
70
+ * The rendering is intentionally generic: it does not have per-tool
71
+ * templates. A `# Tool name` header, a `## Key` for each top-level field, and
72
+ * smart formatting for arrays of objects (tables when the rows share a
73
+ * schema) and scalars. Per-tool overrides can land in v1.9.1+ if any
74
+ * specific tool's output deserves a more curated view.
75
+ *
76
+ * Exposed for tests.
77
+ */
78
+ export declare function renderAsMarkdown(value: unknown, toolName: string): string;