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.
- package/CHANGELOG.md +75 -1
- package/README.md +84 -15
- package/USAGE.md +27 -4
- package/dist/cli.js +106 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.js +42 -22
- package/dist/index.js.map +1 -1
- package/dist/parsers/referenceTree.d.ts +111 -0
- package/dist/parsers/referenceTree.js +328 -0
- package/dist/parsers/referenceTree.js.map +1 -0
- package/dist/runtime/axe.js +6 -1
- package/dist/runtime/axe.js.map +1 -1
- package/dist/runtime/exec.d.ts +30 -0
- package/dist/runtime/exec.js +30 -3
- package/dist/runtime/exec.js.map +1 -1
- package/dist/runtime/fixTemplates.js +67 -0
- package/dist/runtime/fixTemplates.js.map +1 -1
- package/dist/runtime/leakReport.d.ts +62 -0
- package/dist/runtime/leakReport.js +138 -0
- package/dist/runtime/leakReport.js.map +1 -0
- package/dist/runtime/platformCheck.d.ts +54 -0
- package/dist/runtime/platformCheck.js +94 -0
- package/dist/runtime/platformCheck.js.map +1 -0
- package/dist/runtime/redact.d.ts +66 -0
- package/dist/runtime/redact.js +146 -0
- package/dist/runtime/redact.js.map +1 -0
- package/dist/runtime/responseFormatter.d.ts +78 -0
- package/dist/runtime/responseFormatter.js +307 -0
- package/dist/runtime/responseFormatter.js.map +1 -0
- package/dist/runtime/securityFlags.d.ts +74 -0
- package/dist/runtime/securityFlags.js +90 -0
- package/dist/runtime/securityFlags.js.map +1 -0
- package/dist/runtime/staticAnalysisHints.js +14 -1
- package/dist/runtime/staticAnalysisHints.js.map +1 -1
- package/dist/templates/leak-report.html +39 -0
- package/dist/templates/templates/leak-report.html +39 -0
- package/dist/tools/analyzeAbandonedMemory.d.ts +162 -0
- package/dist/tools/analyzeAbandonedMemory.js +325 -0
- package/dist/tools/analyzeAbandonedMemory.js.map +1 -0
- package/dist/tools/analyzeAllocations.d.ts +11 -2
- package/dist/tools/analyzeAllocations.js +4 -0
- package/dist/tools/analyzeAllocations.js.map +1 -1
- package/dist/tools/analyzeAnimationHitches.d.ts +32 -2
- package/dist/tools/analyzeAnimationHitches.js +25 -4
- package/dist/tools/analyzeAnimationHitches.js.map +1 -1
- package/dist/tools/analyzeAppLaunch.d.ts +3 -0
- package/dist/tools/analyzeAppLaunch.js +2 -0
- package/dist/tools/analyzeAppLaunch.js.map +1 -1
- package/dist/tools/analyzeHangs.d.ts +78 -2
- package/dist/tools/analyzeHangs.js +117 -4
- package/dist/tools/analyzeHangs.js.map +1 -1
- package/dist/tools/analyzeMemgraph.d.ts +40 -1
- package/dist/tools/analyzeMemgraph.js +66 -2
- package/dist/tools/analyzeMemgraph.js.map +1 -1
- package/dist/tools/analyzeTimeProfile.d.ts +11 -1
- package/dist/tools/analyzeTimeProfile.js +5 -0
- package/dist/tools/analyzeTimeProfile.js.map +1 -1
- package/dist/tools/bootAndLaunchForLeakInvestigation.d.ts +18 -9
- package/dist/tools/bootAndLaunchForLeakInvestigation.js +27 -0
- package/dist/tools/bootAndLaunchForLeakInvestigation.js.map +1 -1
- package/dist/tools/captureMemgraph.d.ts +22 -4
- package/dist/tools/captureMemgraph.js +42 -9
- package/dist/tools/captureMemgraph.js.map +1 -1
- package/dist/tools/captureScenarioState.d.ts +12 -4
- package/dist/tools/captureScenarioState.js +4 -0
- package/dist/tools/captureScenarioState.js.map +1 -1
- package/dist/tools/classifyCycle.js +77 -0
- package/dist/tools/classifyCycle.js.map +1 -1
- package/dist/tools/cleanupTraces.d.ts +87 -0
- package/dist/tools/cleanupTraces.js +232 -0
- package/dist/tools/cleanupTraces.js.map +1 -0
- package/dist/tools/compareTracesByPattern.d.ts +2 -2
- package/dist/tools/detectLeaksInXCTest.d.ts +116 -0
- package/dist/tools/detectLeaksInXCTest.js +311 -0
- package/dist/tools/detectLeaksInXCTest.js.map +1 -0
- package/dist/tools/detectLeaksInXCUITest.d.ts +8 -3
- package/dist/tools/detectLeaksInXCUITest.js +30 -4
- package/dist/tools/detectLeaksInXCUITest.js.map +1 -1
- package/dist/tools/diffMemgraphs.d.ts +5 -2
- package/dist/tools/diffMemgraphs.js +2 -0
- package/dist/tools/diffMemgraphs.js.map +1 -1
- package/dist/tools/findCycles.d.ts +1 -1
- package/dist/tools/recordTimeProfile.d.ts +29 -9
- package/dist/tools/recordTimeProfile.js +70 -7
- package/dist/tools/recordTimeProfile.js.map +1 -1
- package/dist/tools/renderCycleGraph.d.ts +1 -1
- package/dist/tools/verifyFix.d.ts +2 -2
- package/dist/types.d.ts +24 -1
- 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("&", "&")
|
|
39
|
+
.replaceAll("<", "<")
|
|
40
|
+
.replaceAll(">", ">")
|
|
41
|
+
.replaceAll('"', """)
|
|
42
|
+
.replaceAll("'", "'");
|
|
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;
|