@memlab/mcp-server 2.18.1 → 2.86.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/README.md +30 -16
- package/bin/.eslintrc.js +26 -0
- package/bin/memlab-cli.js +313 -0
- package/dist/anonymized-snapshot.d.ts +75 -0
- package/dist/anonymized-snapshot.d.ts.map +1 -0
- package/dist/anonymized-snapshot.js +101 -0
- package/dist/anonymized-snapshot.js.map +1 -0
- package/dist/artifact-classes.d.ts +86 -0
- package/dist/artifact-classes.d.ts.map +1 -0
- package/dist/artifact-classes.js +261 -0
- package/dist/artifact-classes.js.map +1 -0
- package/dist/capture-mode.d.ts +60 -0
- package/dist/capture-mode.d.ts.map +1 -0
- package/dist/capture-mode.js +39 -0
- package/dist/capture-mode.js.map +1 -0
- package/dist/eval-hints.d.ts +41 -0
- package/dist/eval-hints.d.ts.map +1 -0
- package/dist/eval-hints.js +115 -0
- package/dist/eval-hints.js.map +1 -0
- package/dist/guardrail.d.ts.map +1 -1
- package/dist/guardrail.js +13 -0
- package/dist/guardrail.js.map +1 -1
- package/dist/heap-budget.d.ts +32 -0
- package/dist/heap-budget.d.ts.map +1 -0
- package/dist/heap-budget.js +90 -0
- package/dist/heap-budget.js.map +1 -0
- package/dist/heap-shapes.d.ts +148 -0
- package/dist/heap-shapes.d.ts.map +1 -0
- package/dist/heap-shapes.js +193 -0
- package/dist/heap-shapes.js.map +1 -0
- package/dist/heap-state.d.ts +109 -2
- package/dist/heap-state.d.ts.map +1 -1
- package/dist/heap-state.js +194 -3
- package/dist/heap-state.js.map +1 -1
- package/dist/index.js +102 -1
- package/dist/index.js.map +1 -1
- package/dist/progress.d.ts +25 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +46 -0
- package/dist/progress.js.map +1 -0
- package/dist/react-shapes.d.ts +42 -0
- package/dist/react-shapes.d.ts.map +1 -0
- package/dist/react-shapes.js +149 -0
- package/dist/react-shapes.js.map +1 -0
- package/dist/result-handles.d.ts +11 -0
- package/dist/result-handles.d.ts.map +1 -0
- package/dist/result-handles.js +77 -0
- package/dist/result-handles.js.map +1 -0
- package/dist/server-version.d.ts +19 -0
- package/dist/server-version.d.ts.map +1 -0
- package/dist/server-version.js +19 -0
- package/dist/server-version.js.map +1 -0
- package/dist/snapshot-borrow.d.ts +67 -0
- package/dist/snapshot-borrow.d.ts.map +1 -0
- package/dist/snapshot-borrow.js +113 -0
- package/dist/snapshot-borrow.js.map +1 -0
- package/dist/string-index.d.ts +55 -0
- package/dist/string-index.d.ts.map +1 -0
- package/dist/string-index.js +99 -0
- package/dist/string-index.js.map +1 -0
- package/dist/tool-registry.d.ts +42 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +20 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/tools/analyze-run.d.ts +12 -0
- package/dist/tools/analyze-run.d.ts.map +1 -0
- package/dist/tools/analyze-run.js +404 -0
- package/dist/tools/analyze-run.js.map +1 -0
- package/dist/tools/anonymize-snapshot.d.ts +12 -0
- package/dist/tools/anonymize-snapshot.d.ts.map +1 -0
- package/dist/tools/anonymize-snapshot.js +159 -0
- package/dist/tools/anonymize-snapshot.js.map +1 -0
- package/dist/tools/app-config.d.ts +12 -0
- package/dist/tools/app-config.d.ts.map +1 -0
- package/dist/tools/app-config.js +217 -0
- package/dist/tools/app-config.js.map +1 -0
- package/dist/tools/app-heap.d.ts +30 -0
- package/dist/tools/app-heap.d.ts.map +1 -0
- package/dist/tools/app-heap.js +106 -0
- package/dist/tools/app-heap.js.map +1 -0
- package/dist/tools/artifact-budget.d.ts +12 -0
- package/dist/tools/artifact-budget.d.ts.map +1 -0
- package/dist/tools/artifact-budget.js +219 -0
- package/dist/tools/artifact-budget.js.map +1 -0
- package/dist/tools/async-census.d.ts +12 -0
- package/dist/tools/async-census.d.ts.map +1 -0
- package/dist/tools/async-census.js +243 -0
- package/dist/tools/async-census.js.map +1 -0
- package/dist/tools/auto-investigate.d.ts.map +1 -1
- package/dist/tools/auto-investigate.js +54 -5
- package/dist/tools/auto-investigate.js.map +1 -1
- package/dist/tools/batch.d.ts +12 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +196 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/cache-analysis.d.ts +15 -0
- package/dist/tools/cache-analysis.d.ts.map +1 -1
- package/dist/tools/cache-analysis.js +240 -14
- package/dist/tools/cache-analysis.js.map +1 -1
- package/dist/tools/census-diff.d.ts +55 -0
- package/dist/tools/census-diff.d.ts.map +1 -0
- package/dist/tools/census-diff.js +228 -0
- package/dist/tools/census-diff.js.map +1 -0
- package/dist/tools/chain-walk.d.ts +12 -0
- package/dist/tools/chain-walk.d.ts.map +1 -0
- package/dist/tools/chain-walk.js +184 -0
- package/dist/tools/chain-walk.js.map +1 -0
- package/dist/tools/class-histogram.d.ts.map +1 -1
- package/dist/tools/class-histogram.js +14 -3
- package/dist/tools/class-histogram.js.map +1 -1
- package/dist/tools/collection-diff.d.ts +44 -0
- package/dist/tools/collection-diff.d.ts.map +1 -0
- package/dist/tools/collection-diff.js +343 -0
- package/dist/tools/collection-diff.js.map +1 -0
- package/dist/tools/collection-trend.d.ts +55 -0
- package/dist/tools/collection-trend.d.ts.map +1 -0
- package/dist/tools/collection-trend.js +315 -0
- package/dist/tools/collection-trend.js.map +1 -0
- package/dist/tools/detached-dom.d.ts +5 -0
- package/dist/tools/detached-dom.d.ts.map +1 -1
- package/dist/tools/detached-dom.js +368 -22
- package/dist/tools/detached-dom.js.map +1 -1
- package/dist/tools/dev-artifacts.d.ts +62 -6
- package/dist/tools/dev-artifacts.d.ts.map +1 -1
- package/dist/tools/dev-artifacts.js +685 -24
- package/dist/tools/dev-artifacts.js.map +1 -1
- package/dist/tools/diff-snapshots.d.ts.map +1 -1
- package/dist/tools/diff-snapshots.js +18 -2
- package/dist/tools/diff-snapshots.js.map +1 -1
- package/dist/tools/dom-audit.d.ts +37 -0
- package/dist/tools/dom-audit.d.ts.map +1 -0
- package/dist/tools/dom-audit.js +247 -0
- package/dist/tools/dom-audit.js.map +1 -0
- package/dist/tools/dominator-attribution.d.ts +12 -0
- package/dist/tools/dominator-attribution.d.ts.map +1 -0
- package/dist/tools/dominator-attribution.js +189 -0
- package/dist/tools/dominator-attribution.js.map +1 -0
- package/dist/tools/dominator-chain.d.ts +12 -0
- package/dist/tools/dominator-chain.d.ts.map +1 -0
- package/dist/tools/dominator-chain.js +92 -0
- package/dist/tools/dominator-chain.js.map +1 -0
- package/dist/tools/duplicate-objects.d.ts +12 -0
- package/dist/tools/duplicate-objects.d.ts.map +1 -0
- package/dist/tools/duplicate-objects.js +172 -0
- package/dist/tools/duplicate-objects.js.map +1 -0
- package/dist/tools/duplicated-strings.d.ts.map +1 -1
- package/dist/tools/duplicated-strings.js +49 -40
- package/dist/tools/duplicated-strings.js.map +1 -1
- package/dist/tools/eval-across.d.ts +12 -0
- package/dist/tools/eval-across.d.ts.map +1 -0
- package/dist/tools/eval-across.js +135 -0
- package/dist/tools/eval-across.js.map +1 -0
- package/dist/tools/eval.d.ts +79 -0
- package/dist/tools/eval.d.ts.map +1 -1
- package/dist/tools/eval.js +2523 -197
- package/dist/tools/eval.js.map +1 -1
- package/dist/tools/event-listener-leaks.d.ts.map +1 -1
- package/dist/tools/event-listener-leaks.js +42 -17
- package/dist/tools/event-listener-leaks.js.map +1 -1
- package/dist/tools/event-registry.d.ts.map +1 -1
- package/dist/tools/event-registry.js +42 -8
- package/dist/tools/event-registry.js.map +1 -1
- package/dist/tools/explain-delta.d.ts +12 -0
- package/dist/tools/explain-delta.d.ts.map +1 -0
- package/dist/tools/explain-delta.js +221 -0
- package/dist/tools/explain-delta.js.map +1 -0
- package/dist/tools/find-nodes-by-class.d.ts.map +1 -1
- package/dist/tools/find-nodes-by-class.js +112 -13
- package/dist/tools/find-nodes-by-class.js.map +1 -1
- package/dist/tools/finding-index.d.ts +203 -0
- package/dist/tools/finding-index.d.ts.map +1 -0
- package/dist/tools/finding-index.js +566 -0
- package/dist/tools/finding-index.js.map +1 -0
- package/dist/tools/for-each.d.ts.map +1 -1
- package/dist/tools/for-each.js +15 -3
- package/dist/tools/for-each.js.map +1 -1
- package/dist/tools/get-referrers.d.ts.map +1 -1
- package/dist/tools/get-referrers.js +17 -0
- package/dist/tools/get-referrers.js.map +1 -1
- package/dist/tools/get-value.d.ts +16 -0
- package/dist/tools/get-value.d.ts.map +1 -1
- package/dist/tools/get-value.js +96 -2
- package/dist/tools/get-value.js.map +1 -1
- package/dist/tools/hunt-report.d.ts +12 -0
- package/dist/tools/hunt-report.d.ts.map +1 -0
- package/dist/tools/hunt-report.js +186 -0
- package/dist/tools/hunt-report.js.map +1 -0
- package/dist/tools/hypothesis.d.ts +12 -0
- package/dist/tools/hypothesis.d.ts.map +1 -0
- package/dist/tools/hypothesis.js +280 -0
- package/dist/tools/hypothesis.js.map +1 -0
- package/dist/tools/id-space-audit.d.ts +12 -0
- package/dist/tools/id-space-audit.d.ts.map +1 -0
- package/dist/tools/id-space-audit.js +262 -0
- package/dist/tools/id-space-audit.js.map +1 -0
- package/dist/tools/identify.d.ts +12 -0
- package/dist/tools/identify.d.ts.map +1 -0
- package/dist/tools/identify.js +205 -0
- package/dist/tools/identify.js.map +1 -0
- package/dist/tools/identity-diff.d.ts +12 -0
- package/dist/tools/identity-diff.d.ts.map +1 -0
- package/dist/tools/identity-diff.js +188 -0
- package/dist/tools/identity-diff.js.map +1 -0
- package/dist/tools/intern-opportunities.d.ts.map +1 -1
- package/dist/tools/intern-opportunities.js +116 -50
- package/dist/tools/intern-opportunities.js.map +1 -1
- package/dist/tools/ladder-probe.d.ts +65 -0
- package/dist/tools/ladder-probe.d.ts.map +1 -0
- package/dist/tools/ladder-probe.js +445 -0
- package/dist/tools/ladder-probe.js.map +1 -0
- package/dist/tools/ladder.d.ts +33 -0
- package/dist/tools/ladder.d.ts.map +1 -0
- package/dist/tools/ladder.js +266 -0
- package/dist/tools/ladder.js.map +1 -0
- package/dist/tools/largest-objects.d.ts.map +1 -1
- package/dist/tools/largest-objects.js +6 -2
- package/dist/tools/largest-objects.js.map +1 -1
- package/dist/tools/leak-report.d.ts +35 -0
- package/dist/tools/leak-report.d.ts.map +1 -0
- package/dist/tools/leak-report.js +398 -0
- package/dist/tools/leak-report.js.map +1 -0
- package/dist/tools/load-snapshot.d.ts +29 -0
- package/dist/tools/load-snapshot.d.ts.map +1 -1
- package/dist/tools/load-snapshot.js +294 -23
- package/dist/tools/load-snapshot.js.map +1 -1
- package/dist/tools/map-entries.d.ts +12 -0
- package/dist/tools/map-entries.d.ts.map +1 -0
- package/dist/tools/map-entries.js +139 -0
- package/dist/tools/map-entries.js.map +1 -0
- package/dist/tools/match-object.d.ts +12 -0
- package/dist/tools/match-object.d.ts.map +1 -0
- package/dist/tools/match-object.js +165 -0
- package/dist/tools/match-object.js.map +1 -0
- package/dist/tools/metric.d.ts +13 -0
- package/dist/tools/metric.d.ts.map +1 -0
- package/dist/tools/metric.js +185 -0
- package/dist/tools/metric.js.map +1 -0
- package/dist/tools/module-attribution.d.ts +36 -0
- package/dist/tools/module-attribution.d.ts.map +1 -0
- package/dist/tools/module-attribution.js +235 -0
- package/dist/tools/module-attribution.js.map +1 -0
- package/dist/tools/next-measurement.d.ts +26 -0
- package/dist/tools/next-measurement.d.ts.map +1 -0
- package/dist/tools/next-measurement.js +149 -0
- package/dist/tools/next-measurement.js.map +1 -0
- package/dist/tools/object-cost-breakdown.d.ts.map +1 -1
- package/dist/tools/object-cost-breakdown.js +64 -0
- package/dist/tools/object-cost-breakdown.js.map +1 -1
- package/dist/tools/object-shape.d.ts.map +1 -1
- package/dist/tools/object-shape.js +53 -5
- package/dist/tools/object-shape.js.map +1 -1
- package/dist/tools/population-diff.d.ts +57 -0
- package/dist/tools/population-diff.d.ts.map +1 -0
- package/dist/tools/population-diff.js +338 -0
- package/dist/tools/population-diff.js.map +1 -0
- package/dist/tools/population-vs-owners.d.ts +12 -0
- package/dist/tools/population-vs-owners.d.ts.map +1 -0
- package/dist/tools/population-vs-owners.js +183 -0
- package/dist/tools/population-vs-owners.js.map +1 -0
- package/dist/tools/property-names.d.ts +29 -0
- package/dist/tools/property-names.d.ts.map +1 -0
- package/dist/tools/property-names.js +131 -0
- package/dist/tools/property-names.js.map +1 -0
- package/dist/tools/quick-diagnosis.d.ts.map +1 -1
- package/dist/tools/quick-diagnosis.js +7 -1
- package/dist/tools/quick-diagnosis.js.map +1 -1
- package/dist/tools/react-owners.d.ts +12 -0
- package/dist/tools/react-owners.d.ts.map +1 -0
- package/dist/tools/react-owners.js +167 -0
- package/dist/tools/react-owners.js.map +1 -0
- package/dist/tools/react-update-queues.d.ts +12 -0
- package/dist/tools/react-update-queues.d.ts.map +1 -0
- package/dist/tools/react-update-queues.js +173 -0
- package/dist/tools/react-update-queues.js.map +1 -0
- package/dist/tools/replicate.d.ts +49 -0
- package/dist/tools/replicate.d.ts.map +1 -0
- package/dist/tools/replicate.js +241 -0
- package/dist/tools/replicate.js.map +1 -0
- package/dist/tools/retainer-diff.d.ts +12 -0
- package/dist/tools/retainer-diff.d.ts.map +1 -0
- package/dist/tools/retainer-diff.js +168 -0
- package/dist/tools/retainer-diff.js.map +1 -0
- package/dist/tools/retainer-layers.d.ts +23 -0
- package/dist/tools/retainer-layers.d.ts.map +1 -0
- package/dist/tools/retainer-layers.js +179 -0
- package/dist/tools/retainer-layers.js.map +1 -0
- package/dist/tools/retainer-summary.d.ts +18 -0
- package/dist/tools/retainer-summary.d.ts.map +1 -1
- package/dist/tools/retainer-summary.js +210 -51
- package/dist/tools/retainer-summary.js.map +1 -1
- package/dist/tools/retainer-trace.d.ts.map +1 -1
- package/dist/tools/retainer-trace.js +14 -2
- package/dist/tools/retainer-trace.js.map +1 -1
- package/dist/tools/round-audit.d.ts +66 -0
- package/dist/tools/round-audit.d.ts.map +1 -0
- package/dist/tools/round-audit.js +258 -0
- package/dist/tools/round-audit.js.map +1 -0
- package/dist/tools/round-digest.d.ts +12 -0
- package/dist/tools/round-digest.d.ts.map +1 -0
- package/dist/tools/round-digest.js +190 -0
- package/dist/tools/round-digest.js.map +1 -0
- package/dist/tools/script-census.d.ts +12 -0
- package/dist/tools/script-census.d.ts.map +1 -0
- package/dist/tools/script-census.js +182 -0
- package/dist/tools/script-census.js.map +1 -0
- package/dist/tools/search-nodes.d.ts.map +1 -1
- package/dist/tools/search-nodes.js +5 -1
- package/dist/tools/search-nodes.js.map +1 -1
- package/dist/tools/search-strings.d.ts.map +1 -1
- package/dist/tools/search-strings.js +53 -7
- package/dist/tools/search-strings.js.map +1 -1
- package/dist/tools/sequence-analysis.d.ts +53 -0
- package/dist/tools/sequence-analysis.d.ts.map +1 -1
- package/dist/tools/sequence-analysis.js +313 -89
- package/dist/tools/sequence-analysis.js.map +1 -1
- package/dist/tools/server-status.d.ts.map +1 -1
- package/dist/tools/server-status.js +4 -1
- package/dist/tools/server-status.js.map +1 -1
- package/dist/tools/settle-check.d.ts +12 -0
- package/dist/tools/settle-check.d.ts.map +1 -0
- package/dist/tools/settle-check.js +188 -0
- package/dist/tools/settle-check.js.map +1 -0
- package/dist/tools/shape-histogram.d.ts.map +1 -1
- package/dist/tools/shape-histogram.js +9 -2
- package/dist/tools/shape-histogram.js.map +1 -1
- package/dist/tools/sliced-strings.d.ts.map +1 -1
- package/dist/tools/sliced-strings.js +2 -1
- package/dist/tools/sliced-strings.js.map +1 -1
- package/dist/tools/snapshots.d.ts.map +1 -1
- package/dist/tools/snapshots.js +24 -6
- package/dist/tools/snapshots.js.map +1 -1
- package/dist/tools/sparse-elements.d.ts +12 -0
- package/dist/tools/sparse-elements.d.ts.map +1 -0
- package/dist/tools/sparse-elements.js +215 -0
- package/dist/tools/sparse-elements.js.map +1 -0
- package/dist/tools/stale-collections.d.ts +17 -0
- package/dist/tools/stale-collections.d.ts.map +1 -1
- package/dist/tools/stale-collections.js +28 -0
- package/dist/tools/stale-collections.js.map +1 -1
- package/dist/tools/string-patterns.d.ts.map +1 -1
- package/dist/tools/string-patterns.js +7 -5
- package/dist/tools/string-patterns.js.map +1 -1
- package/dist/tools/tools-index.d.ts +12 -0
- package/dist/tools/tools-index.d.ts.map +1 -0
- package/dist/tools/tools-index.js +492 -0
- package/dist/tools/tools-index.js.map +1 -0
- package/dist/tools/trace-all.d.ts +42 -0
- package/dist/tools/trace-all.d.ts.map +1 -0
- package/dist/tools/trace-all.js +263 -0
- package/dist/tools/trace-all.js.map +1 -0
- package/dist/tools/unit-cost.d.ts +12 -0
- package/dist/tools/unit-cost.d.ts.map +1 -0
- package/dist/tools/unit-cost.js +219 -0
- package/dist/tools/unit-cost.js.map +1 -0
- package/dist/tools/verify-fix.d.ts +23 -0
- package/dist/tools/verify-fix.d.ts.map +1 -0
- package/dist/tools/verify-fix.js +219 -0
- package/dist/tools/verify-fix.js.map +1 -0
- package/dist/tools/weakmap-entries.d.ts.map +1 -1
- package/dist/tools/weakmap-entries.js +6 -1
- package/dist/tools/weakmap-entries.js.map +1 -1
- package/dist/tools/weakref-census.d.ts +15 -0
- package/dist/tools/weakref-census.d.ts.map +1 -0
- package/dist/tools/weakref-census.js +147 -0
- package/dist/tools/weakref-census.js.map +1 -0
- package/dist/tools/what-if.d.ts +12 -0
- package/dist/tools/what-if.d.ts.map +1 -0
- package/dist/tools/what-if.js +285 -0
- package/dist/tools/what-if.js.map +1 -0
- package/dist/utils.d.ts +238 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +507 -15
- package/dist/utils.js.map +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -76,21 +76,12 @@ Then configure (`~/.claude.json` for Claude Code, or `.mcp.json` for Cursor/Wind
|
|
|
76
76
|
|
|
77
77
|
## Auto-Approving Tool Permissions
|
|
78
78
|
|
|
79
|
-
By default, Claude Code prompts you to approve each MCP tool call.
|
|
79
|
+
By default, Claude Code prompts you to approve each MCP tool call. Every MemLab tool except `memlab_eval` and `memlab_for_each` is strictly read-only heap analysis and safe to auto-approve.
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"permissions": {
|
|
84
|
-
"allow": [
|
|
85
|
-
"mcp__memlab__*"
|
|
86
|
-
]
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
```
|
|
81
|
+
> [!CAUTION]
|
|
82
|
+
> **Do not auto-approve `memlab_eval` or `memlab_for_each`.** They execute arbitrary JavaScript with the full privileges of the MCP server process (see the warning under [`memlab_eval`](#memlab_eval)). Leave them on manual approval so each invocation is reviewed. Avoid the `mcp__memlab__*` wildcard, which would auto-approve them too.
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
You can also allowlist individual tools for granular control, add this to your `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level):
|
|
84
|
+
To auto-approve the read-only tools while keeping the two code-execution tools on manual approval, allowlist the read-only tools explicitly in your `~/.claude/settings.json` (user-level) or `.claude/settings.json` (project-level):
|
|
94
85
|
|
|
95
86
|
```json
|
|
96
87
|
{
|
|
@@ -105,7 +96,11 @@ You can also allowlist individual tools for granular control, add this to your `
|
|
|
105
96
|
|
|
106
97
|
## How It Works
|
|
107
98
|
|
|
108
|
-
The server holds
|
|
99
|
+
The server holds loaded `IHeapSnapshot` graphs in memory across tool calls (loading is
|
|
100
|
+
expensive for large heaps). Several snapshots can be resident at once (`keep_previous: true`);
|
|
101
|
+
tools read the active one, which `memlab_snapshots` switches. When a load's projected working
|
|
102
|
+
set exceeds the free old-space budget, the least-recently-used resident snapshot is evicted
|
|
103
|
+
before the load is refused. All tools are read-only — they analyze the heap but never modify it.
|
|
109
104
|
|
|
110
105
|
## Getting a Heap Snapshot
|
|
111
106
|
|
|
@@ -129,11 +124,14 @@ console.log(`Heap snapshot written to ${snapshot}`);
|
|
|
129
124
|
### `memlab_load_snapshot`
|
|
130
125
|
|
|
131
126
|
Load and parse a `.heapsnapshot` file. Builds indexes, computes the dominator tree, and calculates retained sizes. `file_path` may be a local absolute path, a `manifold://bucket/key` URL, or a bare snapshot filename (resolved against the `nest_server_nodejs_heap_snapshots` bucket and fetched via `manifold get`). Pass `keep_previous: true` to keep earlier snapshots resident for diffing/comparison (each gets a handle; manage with `memlab_snapshots`). `quiet` / `suppress_suggestions` set session-wide output controls to trim repeated boilerplate.
|
|
127
|
+
Pass `light: true` for a count-only load that skips the dominator / retained-size /
|
|
128
|
+
shortest-path pass; tools needing retained sizes, dominators or retainer paths then return an
|
|
129
|
+
error naming the light-safe tools instead of reporting zeros.
|
|
132
130
|
|
|
133
131
|
```
|
|
134
132
|
Input: { file_path: "snap.heapsnapshot" | "/abs/path" | "manifold://bucket/key",
|
|
135
133
|
alias?: "before", keep_previous?: false, quiet?: false,
|
|
136
|
-
suppress_suggestions?: false, max_file_size_mb?: 900 }
|
|
134
|
+
suppress_suggestions?: false, light?: false, max_file_size_mb?: 900 }
|
|
137
135
|
Output: { status, file_path, node_count, edge_count, total_size, handle }
|
|
138
136
|
```
|
|
139
137
|
|
|
@@ -157,6 +155,19 @@ Input: { action?: "list"|"switch"|"unload", handle?: "before",
|
|
|
157
155
|
Output: resident snapshots (active one marked), or switch/unload result
|
|
158
156
|
```
|
|
159
157
|
|
|
158
|
+
### `memlab_batch`
|
|
159
|
+
|
|
160
|
+
Run several tools against ONE snapshot load and return all their outputs together, instead of
|
|
161
|
+
re-paying the multi-minute parse on every call. Steps run in order against the resident snapshot
|
|
162
|
+
and see each other's side effects. An unknown tool name is rejected before any step runs, and by
|
|
163
|
+
default a failing step is recorded and the batch continues.
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
Input: { load?: { ...memlab_load_snapshot args }, steps: [{ tool, args? }],
|
|
167
|
+
stop_on_error?: false }
|
|
168
|
+
Output: a per-step output block for each step, headed by a run/failed step count
|
|
169
|
+
```
|
|
170
|
+
|
|
160
171
|
### `memlab_property_distribution`
|
|
161
172
|
|
|
162
173
|
For a class/shape and a property, report value cardinality plus the top-K most frequent values. The key tool for diagnosing cardinality explosions (OTel metric attributes, cache keys, per-record fields).
|
|
@@ -371,7 +382,10 @@ Input: { report: "list"|"full_analysis"|"detached_dom"|"duplicated_strings"|...
|
|
|
371
382
|
|
|
372
383
|
### `memlab_eval`
|
|
373
384
|
|
|
374
|
-
Execute arbitrary JavaScript against the loaded heap snapshot
|
|
385
|
+
Execute arbitrary JavaScript against the loaded heap snapshot. Has access to `snapshot`, `utils`, and `helpers`.
|
|
386
|
+
|
|
387
|
+
> [!WARNING]
|
|
388
|
+
> **This tool runs arbitrary code with the full privileges of the MCP server process.** It uses `node:vm` to scope the globals it injects, but [`node:vm` is not a security sandbox](https://nodejs.org/api/vm.html) — code can reach the host realm (e.g. `Object.constructor('return process')()`) and from there touch the filesystem, network, and shell. Treat calling `memlab_eval` as equivalent to running code on your machine. The same applies to `memlab_for_each`, whose `filter`/`map`/`reduce` code strings are executed the same way. Do not auto-approve these two tools, and be aware that heap snapshot contents are attacker-influenceable input — a prompt-injection could steer the model into running hostile code here.
|
|
375
389
|
|
|
376
390
|
```
|
|
377
391
|
Input: { code: "...", timeout_ms?: 30000 }
|
package/bin/.eslintrc.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Files in this directory are npm `bin` entry points: package.json declares
|
|
12
|
+
// them as executables, so POSIX requires `#!/usr/bin/env node` on line 1.
|
|
13
|
+
//
|
|
14
|
+
// `fb-www/docblock-first` treats anything preceding the file docblock as
|
|
15
|
+
// disqualifying, and the shebang counts — verified by removing it, at which
|
|
16
|
+
// point the rule stops firing. The two requirements are therefore mutually
|
|
17
|
+
// exclusive for this directory, and the rule's stated rationale (ESLint skips
|
|
18
|
+
// pragma parsing, producing spurious no-undef errors for Flow utility types)
|
|
19
|
+
// does not apply: these are plain CommonJS/ESM Node scripts with no Flow types.
|
|
20
|
+
//
|
|
21
|
+
// Scoped to bin/ so the rule keeps applying to every other file in the package.
|
|
22
|
+
module.exports = {
|
|
23
|
+
rules: {
|
|
24
|
+
'fb-www/docblock-first': 'off',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
/// usr/bin/env node
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Non-MCP command line for the memlab MCP server.
|
|
14
|
+
*
|
|
15
|
+
* Why this exists: the server is only ever reachable through an MCP client, so
|
|
16
|
+
* when a host fails to attach it (a dropped stdio handshake, a plugin that never
|
|
17
|
+
* finishes connecting) every tool in the package becomes unavailable at once,
|
|
18
|
+
* with no diagnostic and no fallback. That happened in practice and cost an
|
|
19
|
+
* agent ~20 minutes and a hand-written JSON-RPC client before any heap could be
|
|
20
|
+
* analyzed at all. The server itself was healthy the whole time.
|
|
21
|
+
*
|
|
22
|
+
* This speaks the same stdio JSON-RPC the host would, so the tools stay usable
|
|
23
|
+
* without one:
|
|
24
|
+
*
|
|
25
|
+
* memlab-cli doctor
|
|
26
|
+
* memlab-cli list
|
|
27
|
+
* memlab-cli schema memlab_load_snapshot
|
|
28
|
+
* memlab-cli call memlab_snapshot_header '{"file_path":"/tmp/a.heapsnapshot"}'
|
|
29
|
+
* memlab-cli script steps.jsonl # one {"tool":..,"args":{..}} per line
|
|
30
|
+
*
|
|
31
|
+
* `script` (and multiple `call` pairs) reuse ONE server process, which matters:
|
|
32
|
+
* the server is stateful and a large snapshot costs minutes to load, so a
|
|
33
|
+
* process per call pays that repeatedly.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import {spawn} from 'child_process';
|
|
37
|
+
import fs from 'fs';
|
|
38
|
+
import path from 'path';
|
|
39
|
+
import {fileURLToPath} from 'url';
|
|
40
|
+
|
|
41
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
|
|
43
|
+
function resolveServerPath() {
|
|
44
|
+
if (process.env.MEMLAB_MCP_SERVER) {
|
|
45
|
+
return process.env.MEMLAB_MCP_SERVER;
|
|
46
|
+
}
|
|
47
|
+
// Packaged layout is bin/ next to dist/; the plugin copies this script to the
|
|
48
|
+
// install root, where dist/ is a direct child.
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(HERE, '..', 'dist', 'index.js'),
|
|
51
|
+
path.join(HERE, 'dist', 'index.js'),
|
|
52
|
+
];
|
|
53
|
+
for (const c of candidates) {
|
|
54
|
+
if (fs.existsSync(c)) {
|
|
55
|
+
return c;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return candidates[0];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The interpreter to run the SERVER with, which is not necessarily the one
|
|
63
|
+
* running this CLI: `node` on PATH is frequently older than the package's
|
|
64
|
+
* `>= 18` engine requirement, and spawning the server with it fails at module
|
|
65
|
+
* load with an opaque syntax error. Mirrors the plugin start.sh lookup.
|
|
66
|
+
*/
|
|
67
|
+
function resolveNodeBin() {
|
|
68
|
+
if (Number(process.versions.node.split('.')[0]) >= 18) {
|
|
69
|
+
return process.execPath;
|
|
70
|
+
}
|
|
71
|
+
for (const c of ['/usr/local/bin/claude_code/node']) {
|
|
72
|
+
if (fs.existsSync(c)) {
|
|
73
|
+
return c;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return process.execPath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class Client {
|
|
80
|
+
constructor(serverPath) {
|
|
81
|
+
this.proc = spawn(
|
|
82
|
+
resolveNodeBin(),
|
|
83
|
+
['--max-old-space-size=8192', serverPath],
|
|
84
|
+
{stdio: ['pipe', 'pipe', 'pipe']},
|
|
85
|
+
);
|
|
86
|
+
this.nextId = 0;
|
|
87
|
+
this.buf = '';
|
|
88
|
+
this.pending = new Map();
|
|
89
|
+
this.stderr = [];
|
|
90
|
+
// Drain stderr. A chatty server that fills this pipe would otherwise block
|
|
91
|
+
// its own writes and deadlock the session.
|
|
92
|
+
this.proc.stderr.on('data', d => {
|
|
93
|
+
this.stderr.push(String(d));
|
|
94
|
+
if (this.stderr.length > 400) {
|
|
95
|
+
this.stderr.splice(0, 200);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
this.proc.stdout.on('data', d => this.onData(String(d)));
|
|
99
|
+
this.proc.on('exit', code => {
|
|
100
|
+
for (const {reject} of this.pending.values()) {
|
|
101
|
+
reject(
|
|
102
|
+
new Error(
|
|
103
|
+
`memlab server exited (code ${code}). stderr tail:\n${this.stderr.slice(-40).join('')}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
this.pending.clear();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onData(chunk) {
|
|
112
|
+
this.buf += chunk;
|
|
113
|
+
let nl;
|
|
114
|
+
while ((nl = this.buf.indexOf('\n')) >= 0) {
|
|
115
|
+
const line = this.buf.slice(0, nl).trim();
|
|
116
|
+
this.buf = this.buf.slice(nl + 1);
|
|
117
|
+
if (!line) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
let msg;
|
|
121
|
+
try {
|
|
122
|
+
msg = JSON.parse(line);
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const entry = this.pending.get(msg.id);
|
|
127
|
+
if (!entry) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
this.pending.delete(msg.id);
|
|
131
|
+
if (msg.error) {
|
|
132
|
+
entry.reject(new Error(JSON.stringify(msg.error)));
|
|
133
|
+
} else {
|
|
134
|
+
entry.resolve(msg.result);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
request(method, params) {
|
|
140
|
+
const id = ++this.nextId;
|
|
141
|
+
this.proc.stdin.write(
|
|
142
|
+
JSON.stringify({jsonrpc: '2.0', id, method, params}) + '\n',
|
|
143
|
+
);
|
|
144
|
+
return new Promise((resolve, reject) =>
|
|
145
|
+
this.pending.set(id, {resolve, reject}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
notify(method, params) {
|
|
150
|
+
this.proc.stdin.write(
|
|
151
|
+
JSON.stringify({jsonrpc: '2.0', method, params}) + '\n',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async init() {
|
|
156
|
+
await this.request('initialize', {
|
|
157
|
+
protocolVersion: '2024-11-05',
|
|
158
|
+
capabilities: {},
|
|
159
|
+
clientInfo: {name: 'memlab-cli', version: '1.0'},
|
|
160
|
+
});
|
|
161
|
+
this.notify('notifications/initialized', {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async listTools() {
|
|
165
|
+
return (await this.request('tools/list', {})).tools ?? [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async callTool(name, args) {
|
|
169
|
+
const res = await this.request('tools/call', {name, arguments: args});
|
|
170
|
+
return (res.content ?? [])
|
|
171
|
+
.map(c => (c.type === 'text' ? c.text : JSON.stringify(c)))
|
|
172
|
+
.join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
close() {
|
|
176
|
+
this.proc.stdin.end();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function doctor(serverPath) {
|
|
181
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
182
|
+
const nodeBin = resolveNodeBin();
|
|
183
|
+
const lines = [
|
|
184
|
+
`cli node: ${process.versions.node}${major >= 18 ? '' : ' (below the >= 18 engine requirement)'}`,
|
|
185
|
+
`server node: ${nodeBin}${nodeBin === process.execPath && major < 18 ? ' — NO >= 18 interpreter found; the server will fail to load' : ''}`,
|
|
186
|
+
];
|
|
187
|
+
lines.push(`server path: ${serverPath}`);
|
|
188
|
+
const exists = fs.existsSync(serverPath);
|
|
189
|
+
lines.push(`server present: ${exists ? 'yes' : 'NO'}`);
|
|
190
|
+
if (!exists) {
|
|
191
|
+
lines.push(
|
|
192
|
+
'Fix: build the package (`npm run build-pkg`), or set MEMLAB_MCP_SERVER to a built dist/index.js.',
|
|
193
|
+
);
|
|
194
|
+
console.log(lines.join('\n'));
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
// The decisive check: can the server actually start and answer? A corrupted
|
|
198
|
+
// or partial node_modules only shows up here, not from a file listing.
|
|
199
|
+
const client = new Client(serverPath);
|
|
200
|
+
const timer = setTimeout(() => client.proc.kill('SIGKILL'), 60000);
|
|
201
|
+
try {
|
|
202
|
+
await client.init();
|
|
203
|
+
const tools = await client.listTools();
|
|
204
|
+
lines.push(`handshake: OK`);
|
|
205
|
+
lines.push(`tools registered: ${tools.length}`);
|
|
206
|
+
console.log(lines.join('\n'));
|
|
207
|
+
return 0;
|
|
208
|
+
} catch (err) {
|
|
209
|
+
lines.push(`handshake: FAILED — ${err.message}`);
|
|
210
|
+
lines.push(
|
|
211
|
+
'Fix: check the install dir for build-errors.log / deps-check-errors.log, ' +
|
|
212
|
+
'then rebuild (the plugin start.sh does a clean rebuild when dependencies fail to load).',
|
|
213
|
+
);
|
|
214
|
+
console.log(lines.join('\n'));
|
|
215
|
+
return 1;
|
|
216
|
+
} finally {
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
client.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function main() {
|
|
223
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
224
|
+
const serverPath = resolveServerPath();
|
|
225
|
+
|
|
226
|
+
if (!cmd || cmd === '--help' || cmd === '-h') {
|
|
227
|
+
console.log(
|
|
228
|
+
[
|
|
229
|
+
'Usage: memlab-cli <command>',
|
|
230
|
+
'',
|
|
231
|
+
' doctor check node, server presence, and a live handshake',
|
|
232
|
+
' list list tool names and one-line descriptions',
|
|
233
|
+
' schema <tool> print a tool input schema as JSON',
|
|
234
|
+
' call <tool> <json> [...] call one or more tools in ONE server session',
|
|
235
|
+
' script <file.jsonl> run {"tool":..,"args":{..}} lines in ONE session',
|
|
236
|
+
'',
|
|
237
|
+
'Server is resolved from $MEMLAB_MCP_SERVER, else dist/index.js next to this script.',
|
|
238
|
+
].join('\n'),
|
|
239
|
+
);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (cmd === 'doctor') {
|
|
244
|
+
return doctor(serverPath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const client = new Client(serverPath);
|
|
248
|
+
try {
|
|
249
|
+
await client.init();
|
|
250
|
+
if (cmd === 'list') {
|
|
251
|
+
for (const t of await client.listTools()) {
|
|
252
|
+
console.log(`${t.name}\t${(t.description ?? '').slice(0, 150)}`);
|
|
253
|
+
}
|
|
254
|
+
} else if (cmd === 'schema') {
|
|
255
|
+
const want = rest[0];
|
|
256
|
+
const tool = (await client.listTools()).find(t => t.name === want);
|
|
257
|
+
if (!tool) {
|
|
258
|
+
console.error(`Unknown tool: ${want}`);
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
console.log(JSON.stringify(tool.inputSchema ?? {}, null, 2));
|
|
262
|
+
} else if (cmd === 'call') {
|
|
263
|
+
for (let i = 0; i < rest.length; i += 2) {
|
|
264
|
+
const tool = rest[i];
|
|
265
|
+
const args = rest[i + 1] ? JSON.parse(rest[i + 1]) : {};
|
|
266
|
+
console.log(`\n===== ${tool} =====`);
|
|
267
|
+
// Sequential by construction: JSON-RPC over ONE stdio socket to a
|
|
268
|
+
// stateful server (a loaded snapshot). These cannot be parallelised —
|
|
269
|
+
// the server answers one request at a time and later calls depend on
|
|
270
|
+
// earlier ones.
|
|
271
|
+
// eslint-disable-next-line no-await-in-loop
|
|
272
|
+
console.log(await client.callTool(tool, args));
|
|
273
|
+
}
|
|
274
|
+
} else if (cmd === 'script') {
|
|
275
|
+
const file = rest[0];
|
|
276
|
+
// Sequential by construction: these are JSON-RPC calls over ONE stdio
|
|
277
|
+
// socket to a stateful server, so they cannot be parallelised — the next
|
|
278
|
+
// request depends on the previous one having been answered.
|
|
279
|
+
|
|
280
|
+
for (const raw of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
281
|
+
const line = raw.trim();
|
|
282
|
+
if (!line || line.startsWith('#')) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const spec = JSON.parse(line);
|
|
286
|
+
console.log(`\n===== ${spec.tool} =====`);
|
|
287
|
+
// Sequential by construction: JSON-RPC over ONE stdio socket to a
|
|
288
|
+
// stateful server (a loaded snapshot). These cannot be parallelised —
|
|
289
|
+
// the server answers one request at a time and later calls depend on
|
|
290
|
+
// earlier ones.
|
|
291
|
+
// eslint-disable-next-line no-await-in-loop
|
|
292
|
+
console.log(await client.callTool(spec.tool, spec.args ?? {}));
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
console.error(`Unknown command: ${cmd}. Try --help.`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
return 0;
|
|
299
|
+
} finally {
|
|
300
|
+
client.close();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Terminal by construction: both arms call process.exit, so there is nothing
|
|
305
|
+
// left to chain and no rejection can escape.
|
|
306
|
+
// eslint-disable-next-line fb-www/promise-termination
|
|
307
|
+
main().then(
|
|
308
|
+
code => process.exit(code ?? 0),
|
|
309
|
+
err => {
|
|
310
|
+
console.error(err.message ?? String(err));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
import type { IHeapSnapshot } from '@memlab/core';
|
|
11
|
+
/**
|
|
12
|
+
* Has this capture had its string content replaced — and if so, in a way that
|
|
13
|
+
* corrupts the duplication numbers, or not?
|
|
14
|
+
*
|
|
15
|
+
* Anonymisers replace string CONTENT while preserving LENGTH. Whether that
|
|
16
|
+
* matters to an analysis depends entirely on which kind you are looking at, so
|
|
17
|
+
* this reports three states rather than a boolean:
|
|
18
|
+
*
|
|
19
|
+
* - `uniform` — every character replaced by ONE filler character. Every
|
|
20
|
+
* distinct string of a given length collapses into the same value, so every
|
|
21
|
+
* string-duplication analysis measures the anonymiser instead of the app.
|
|
22
|
+
* Measured on one such capture: `memlab_quick_diagnosis` reported **"Total
|
|
23
|
+
* interning savings: 47.4 MB"** and `memlab_duplicated_strings` reported a
|
|
24
|
+
* single 5.2 KB string duplicated 939 times for 42.1 MB. Both were pure
|
|
25
|
+
* artifact. Nothing flagged it; it was caught only because the sample values
|
|
26
|
+
* rendered visibly as runs of `?`.
|
|
27
|
+
* - `stable` — replaced by a value-derived token, so distinctness survives
|
|
28
|
+
* along with length. Duplication and interning figures are as true as on the
|
|
29
|
+
* original capture, and warning about them would make readers discard
|
|
30
|
+
* correct findings. Only the content is unreadable.
|
|
31
|
+
* - `none` — an ordinary capture.
|
|
32
|
+
*
|
|
33
|
+
* Both tests are deliberately cheap and deliberately conservative: sample
|
|
34
|
+
* strings, and only classify when a large majority match. A normal heap has
|
|
35
|
+
* plenty of short repeated-character strings (" ", "----") and plenty of short
|
|
36
|
+
* lowercase words, so the length floor matters more than the ratio for either.
|
|
37
|
+
*/
|
|
38
|
+
export type AnonymizationKind =
|
|
39
|
+
/** ordinary capture */
|
|
40
|
+
'none'
|
|
41
|
+
/**
|
|
42
|
+
* every character replaced by one filler character. Length survives, so
|
|
43
|
+
* distinct values of equal length collapse into one and duplication figures
|
|
44
|
+
* become fiction.
|
|
45
|
+
*/
|
|
46
|
+
| 'uniform'
|
|
47
|
+
/**
|
|
48
|
+
* replaced by a value-derived token. Length AND distinctness survive, so
|
|
49
|
+
* duplication figures stay true; only the content is gone.
|
|
50
|
+
*/
|
|
51
|
+
| 'stable';
|
|
52
|
+
export interface AnonymizationCheck {
|
|
53
|
+
kind: AnonymizationKind;
|
|
54
|
+
/**
|
|
55
|
+
* true only for `uniform`, which is the kind that corrupts duplication
|
|
56
|
+
* numbers. Existing call sites read this to decide whether to warn, and a
|
|
57
|
+
* `stable` capture must NOT trip them — its numbers are trustworthy.
|
|
58
|
+
*/
|
|
59
|
+
anonymized: boolean;
|
|
60
|
+
sampled: number;
|
|
61
|
+
singleCharRuns: number;
|
|
62
|
+
tokenRuns: number;
|
|
63
|
+
exampleChar?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function detectAnonymizedStrings(snapshot: IHeapSnapshot): AnonymizationCheck;
|
|
66
|
+
/** The banner every string-content tool prints when the capture is anonymised. */
|
|
67
|
+
export declare function anonymizedStringsBanner(check: AnonymizationCheck): string;
|
|
68
|
+
/**
|
|
69
|
+
* Prepend the anonymisation banner to a tool's output when it applies.
|
|
70
|
+
*
|
|
71
|
+
* Wraps the result rather than being left to each caller to remember, because
|
|
72
|
+
* "remember to check" is exactly the failure this exists to prevent.
|
|
73
|
+
*/
|
|
74
|
+
export declare function withAnonymizedBanner(snapshot: IHeapSnapshot, text: string): string;
|
|
75
|
+
//# sourceMappingURL=anonymized-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anonymized-snapshot.d.ts","sourceRoot":"","sources":["../src/anonymized-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,cAAc,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,MAAM,iBAAiB;AAC3B,uBAAuB;AACrB,MAAM;AACR;;;;GAIG;GACD,SAAS;AACX;;;GAGG;GACD,QAAQ,CAAC;AAEb,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,iBAAiB,CAAC;IACxB;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAMD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,aAAa,GACtB,kBAAkB,CAqDpB;AAED,kFAAkF;AAClF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,CAwBzE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,MAAM,GACX,MAAM,CAIR"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @format
|
|
8
|
+
* @oncall memory_lab
|
|
9
|
+
*/
|
|
10
|
+
const MIN_LEN = 8;
|
|
11
|
+
const SAMPLE_TARGET = 400;
|
|
12
|
+
const RATIO = 0.8;
|
|
13
|
+
export function detectAnonymizedStrings(snapshot) {
|
|
14
|
+
let sampled = 0;
|
|
15
|
+
let singleCharRuns = 0;
|
|
16
|
+
let tokenRuns = 0;
|
|
17
|
+
let exampleChar;
|
|
18
|
+
let visited = 0;
|
|
19
|
+
snapshot.nodes.forEach(node => {
|
|
20
|
+
if (sampled >= SAMPLE_TARGET)
|
|
21
|
+
return;
|
|
22
|
+
if (node.type !== 'string')
|
|
23
|
+
return;
|
|
24
|
+
// Stride rather than take the first N: the first strings in a heap are
|
|
25
|
+
// V8 internals and interned literals, which are never anonymised and would
|
|
26
|
+
// make every capture look clean.
|
|
27
|
+
visited++;
|
|
28
|
+
if (visited % 7 !== 0)
|
|
29
|
+
return;
|
|
30
|
+
const value = node.name;
|
|
31
|
+
if (typeof value !== 'string' || value.length < MIN_LEN)
|
|
32
|
+
return;
|
|
33
|
+
sampled++;
|
|
34
|
+
const first = value[0];
|
|
35
|
+
let uniform = true;
|
|
36
|
+
for (let i = 1; i < value.length; i++) {
|
|
37
|
+
if (value[i] !== first) {
|
|
38
|
+
uniform = false;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (uniform) {
|
|
43
|
+
singleCharRuns++;
|
|
44
|
+
exampleChar ??= first;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// `stable` fill is a run of lowercase letters and nothing else. Real string
|
|
48
|
+
// values of this length essentially always carry a space, digit, capital or
|
|
49
|
+
// punctuation mark somewhere, so a heap where most of them are bare
|
|
50
|
+
// lowercase runs has been rewritten.
|
|
51
|
+
if (/^[a-z]+$/.test(value)) {
|
|
52
|
+
tokenRuns++;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const uniformRatio = sampled >= 20 ? singleCharRuns / sampled : 0;
|
|
56
|
+
const tokenRatio = sampled >= 20 ? tokenRuns / sampled : 0;
|
|
57
|
+
const kind = uniformRatio >= RATIO ? 'uniform' : tokenRatio >= RATIO ? 'stable' : 'none';
|
|
58
|
+
return {
|
|
59
|
+
kind,
|
|
60
|
+
anonymized: kind === 'uniform',
|
|
61
|
+
sampled,
|
|
62
|
+
singleCharRuns,
|
|
63
|
+
tokenRuns,
|
|
64
|
+
exampleChar,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The banner every string-content tool prints when the capture is anonymised. */
|
|
68
|
+
export function anonymizedStringsBanner(check) {
|
|
69
|
+
if (check.kind === 'stable') {
|
|
70
|
+
// Deliberately NOT a warning about the numbers. `stable` anonymization
|
|
71
|
+
// preserves distinctness, so duplication and interning figures are exactly
|
|
72
|
+
// as true as on the original capture — saying otherwise here would make
|
|
73
|
+
// readers discard correct findings.
|
|
74
|
+
const pct = check.sampled
|
|
75
|
+
? Math.round((check.tokenRuns / check.sampled) * 100)
|
|
76
|
+
: 0;
|
|
77
|
+
return (`> ℹ️ **This capture is ANONYMISED (value-stable) — ${pct}% of sampled string values are opaque tokens.** ` +
|
|
78
|
+
'String CONTENT has been replaced, but lengths and distinctness were preserved, so counts, sizes, duplication and interning figures below are accurate. ' +
|
|
79
|
+
'What you cannot do is read the strings: a sample value tells you nothing about the real data, so identify records by property shape rather than by value.');
|
|
80
|
+
}
|
|
81
|
+
const pct = check.sampled
|
|
82
|
+
? Math.round((check.singleCharRuns / check.sampled) * 100)
|
|
83
|
+
: 0;
|
|
84
|
+
return (`> ⚠️ **This capture looks ANONYMISED — ${pct}% of sampled strings are a single character (\`${check.exampleChar ?? '?'}\`) repeated.** ` +
|
|
85
|
+
'An anonymiser replaces string content but preserves length, so every distinct string of the same length collapses to the same value. ' +
|
|
86
|
+
'**Every duplication and interning figure below is manufactured by that collapse and does not exist in the real app.** ' +
|
|
87
|
+
'Do not quote them, and do not open an interning task from them. Composition, populations, retainer paths and dominator-deduped sizes are unaffected and remain trustworthy.');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Prepend the anonymisation banner to a tool's output when it applies.
|
|
91
|
+
*
|
|
92
|
+
* Wraps the result rather than being left to each caller to remember, because
|
|
93
|
+
* "remember to check" is exactly the failure this exists to prevent.
|
|
94
|
+
*/
|
|
95
|
+
export function withAnonymizedBanner(snapshot, text) {
|
|
96
|
+
const check = detectAnonymizedStrings(snapshot);
|
|
97
|
+
if (check.kind === 'none')
|
|
98
|
+
return text;
|
|
99
|
+
return `${anonymizedStringsBanner(check)}\n\n${text}`;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=anonymized-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anonymized-snapshot.js","sourceRoot":"","sources":["../src/anonymized-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4DH,MAAM,OAAO,GAAG,CAAC,CAAC;AAClB,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,KAAK,GAAG,GAAG,CAAC;AAElB,MAAM,UAAU,uBAAuB,CACrC,QAAuB;IAEvB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,cAAc,GAAG,CAAC,CAAC;IACvB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,WAA+B,CAAC;IACpC,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QAC5B,IAAI,OAAO,IAAI,aAAa;YAAE,OAAO;QACrC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;YAAE,OAAO;QACnC,uEAAuE;QACvE,2EAA2E;QAC3E,iCAAiC;QACjC,OAAO,EAAE,CAAC;QACV,IAAI,OAAO,GAAG,CAAC,KAAK,CAAC;YAAE,OAAO;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;QACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,OAAO;YAAE,OAAO;QAChE,OAAO,EAAE,CAAC;QACV,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC;gBACvB,OAAO,GAAG,KAAK,CAAC;gBAChB,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,cAAc,EAAE,CAAC;YACjB,WAAW,KAAK,KAAK,CAAC;YACtB,OAAO;QACT,CAAC;QACD,4EAA4E;QAC5E,4EAA4E;QAC5E,oEAAoE;QACpE,qCAAqC;QACrC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,SAAS,EAAE,CAAC;QACd,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,YAAY,GAAG,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,MAAM,UAAU,GAAG,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,MAAM,IAAI,GACR,YAAY,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IAE9E,OAAO;QACL,IAAI;QACJ,UAAU,EAAE,IAAI,KAAK,SAAS;QAC9B,OAAO;QACP,cAAc;QACd,SAAS;QACT,WAAW;KACZ,CAAC;AACJ,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,uBAAuB,CAAC,KAAyB;IAC/D,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC5B,uEAAuE;QACvE,2EAA2E;QAC3E,wEAAwE;QACxE,oCAAoC;QACpC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO;YACvB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC;YACrD,CAAC,CAAC,CAAC,CAAC;QACN,OAAO,CACL,sDAAsD,GAAG,kDAAkD;YAC3G,yJAAyJ;YACzJ,2JAA2J,CAC5J,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO;QACvB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC;QAC1D,CAAC,CAAC,CAAC,CAAC;IACN,OAAO,CACL,0CAA0C,GAAG,kDAAkD,KAAK,CAAC,WAAW,IAAI,GAAG,kBAAkB;QACzI,uIAAuI;QACvI,wHAAwH;QACxH,6KAA6K,CAC9K,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAAuB,EACvB,IAAY;IAEZ,MAAM,KAAK,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,GAAG,uBAAuB,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;AACxD,CAAC"}
|