@yawlabs/mcph 0.36.0 → 0.38.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 +8 -0
- package/dist/index.js +156 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@yawlabs/mcph` are documented here. This project uses [semantic versioning](https://semver.org) and a CI-gated release flow: pushing a `vX.Y.Z` tag triggers `.github/workflows/release.yml`, which publishes to npm.
|
|
4
4
|
|
|
5
|
+
## 0.38.0 — 2026-04-18
|
|
6
|
+
|
|
7
|
+
- **`mcph reset-learning` CLI subcommand** — Deletes `~/.mcph/state.json` so cross-session learning starts fresh; prints the entry counts that were cleared. Pairs with v0.37.0's doctor RELIABILITY section: once a namespace has been flagged flaky, the dispatch penalty branch (v0.36.0) keeps suppressing it until enough new successes pile up — but if the user has since fixed the underlying cause (rotated a token, swapped the upstream, re-authed), that history is stale and the penalty has overstayed its welcome. This gives them a direct CLI lever to clear it. Scope is all-or-nothing by design; a per-namespace flag is footgunny (user clears one, forgets the others, keeps getting silently mis-ranked). No-op with an explanatory message when `MCPH_DISABLE_PERSISTENCE` is set or the file doesn't exist, so `mcph reset-learning` never surprises. Exit 0 on success or no-op, exit 1 on I/O error (permissions, disk).
|
|
8
|
+
|
|
9
|
+
## 0.37.0 — 2026-04-18
|
|
10
|
+
|
|
11
|
+
- **`mcph doctor` RELIABILITY section** — New block surfaces flaky dormant namespaces pulled directly from `~/.mcph/state.json`, using the same ≥3-dispatches / <80%-success definition as `mcp_connect_health`'s cross-session reliability block — so the CLI diagnostic and the LLM-facing health tool agree on what "flaky" means. Sorted worst-rate first, capped at 5. Silently omitted when no namespace qualifies, state.json doesn't exist yet, or `MCPH_DISABLE_PERSISTENCE` is set. Threshold constants + sort logic extracted into `selectFlakyNamespaces` so handleHealth and doctor can't drift apart.
|
|
12
|
+
|
|
5
13
|
## 0.36.0 — 2026-04-18
|
|
6
14
|
|
|
7
15
|
- **Negative signal in dispatch ranking (`boostFactor` penalty branch)** — The learning store's `boostFactor` now drops *below* 1.0 for namespaces with flaky history, mirroring the existing upward boost. Threshold is the same ≥3 dispatches / <80% success gate used by discover's inline reliability warning (v0.35.0) and health's cross-session block (v0.34.0) — so a server flagged flaky in those views also loses rank points at dispatch time rather than quietly continuing to win routing. Floor is `-10%` (`LEARNING_MIN_BOOST = 0.9`), symmetric with the existing `+10%` ceiling. Rate-based signal trumps count-based: a namespace with 10 successes but a 50% overall rate is flaky, not useful, and the penalty branch beats the positive branch in that case.
|
package/dist/index.js
CHANGED
|
@@ -945,8 +945,66 @@ function errorMessage(err) {
|
|
|
945
945
|
return err instanceof Error ? err.message : String(err);
|
|
946
946
|
}
|
|
947
947
|
|
|
948
|
+
// src/usage-hints.ts
|
|
949
|
+
var MAX_PEERS = 3;
|
|
950
|
+
var MIN_SUCCESS_TO_SHOW = 1;
|
|
951
|
+
var RELIABILITY_MIN_OBSERVATIONS = 3;
|
|
952
|
+
var RELIABILITY_THRESHOLD = 0.8;
|
|
953
|
+
function buildCoUsageMap(packs) {
|
|
954
|
+
const result = /* @__PURE__ */ new Map();
|
|
955
|
+
for (const pack of packs) {
|
|
956
|
+
for (const ns of pack.namespaces) {
|
|
957
|
+
const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
|
|
958
|
+
for (const peer of pack.namespaces) {
|
|
959
|
+
if (peer !== ns) bucket.add(peer);
|
|
960
|
+
}
|
|
961
|
+
result.set(ns, bucket);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const sorted = /* @__PURE__ */ new Map();
|
|
965
|
+
for (const [ns, peers] of result) {
|
|
966
|
+
sorted.set(ns, Array.from(peers).sort());
|
|
967
|
+
}
|
|
968
|
+
return sorted;
|
|
969
|
+
}
|
|
970
|
+
function formatUsageHint(usage, coUsedWith) {
|
|
971
|
+
const parts = [];
|
|
972
|
+
if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
|
|
973
|
+
parts.push(`used ${usage.succeeded}x`);
|
|
974
|
+
}
|
|
975
|
+
if (coUsedWith.length > 0) {
|
|
976
|
+
const shown = coUsedWith.slice(0, MAX_PEERS);
|
|
977
|
+
const more = coUsedWith.length - shown.length;
|
|
978
|
+
const names = shown.map((n) => `"${n}"`).join(", ");
|
|
979
|
+
const tail = more > 0 ? ` +${more} more` : "";
|
|
980
|
+
parts.push(`often loaded with ${names}${tail}`);
|
|
981
|
+
}
|
|
982
|
+
if (parts.length === 0) return null;
|
|
983
|
+
return `usage: ${parts.join("; ")}`;
|
|
984
|
+
}
|
|
985
|
+
function formatReliabilityWarning(usage) {
|
|
986
|
+
if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
|
|
987
|
+
const rate = usage.succeeded / usage.dispatched;
|
|
988
|
+
if (rate >= RELIABILITY_THRESHOLD) return null;
|
|
989
|
+
const pct = Math.round(rate * 100);
|
|
990
|
+
return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
|
|
991
|
+
}
|
|
992
|
+
function selectFlakyNamespaces(entries, limit) {
|
|
993
|
+
if (limit <= 0) return [];
|
|
994
|
+
return Array.from(entries).filter(({ usage }) => {
|
|
995
|
+
if (usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return false;
|
|
996
|
+
return usage.succeeded / usage.dispatched < RELIABILITY_THRESHOLD;
|
|
997
|
+
}).sort((a, b) => {
|
|
998
|
+
const aRate = a.usage.succeeded / a.usage.dispatched;
|
|
999
|
+
const bRate = b.usage.succeeded / b.usage.dispatched;
|
|
1000
|
+
if (aRate !== bRate) return aRate - bRate;
|
|
1001
|
+
if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
|
|
1002
|
+
return a.namespace.localeCompare(b.namespace);
|
|
1003
|
+
}).slice(0, limit);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
948
1006
|
// src/doctor-cmd.ts
|
|
949
|
-
var VERSION = true ? "0.
|
|
1007
|
+
var VERSION = true ? "0.38.0" : "dev";
|
|
950
1008
|
async function runDoctor(opts = {}) {
|
|
951
1009
|
const lines = [];
|
|
952
1010
|
const write = opts.out ?? ((s) => process.stdout.write(s));
|
|
@@ -983,6 +1041,7 @@ async function runDoctor(opts = {}) {
|
|
|
983
1041
|
print("");
|
|
984
1042
|
renderEnvSection({ env, print });
|
|
985
1043
|
await renderStateSection({ home, env, print });
|
|
1044
|
+
await renderReliabilitySection({ home, env, print });
|
|
986
1045
|
const clients = probeClients({ home, os, cwd });
|
|
987
1046
|
print("INSTALLED CLIENTS (probed config files)");
|
|
988
1047
|
for (const c of clients) {
|
|
@@ -1075,6 +1134,26 @@ async function renderStateSection(opts) {
|
|
|
1075
1134
|
}
|
|
1076
1135
|
print("");
|
|
1077
1136
|
}
|
|
1137
|
+
async function renderReliabilitySection(opts) {
|
|
1138
|
+
const { home, env, print } = opts;
|
|
1139
|
+
const raw = env.MCPH_DISABLE_PERSISTENCE;
|
|
1140
|
+
const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
1141
|
+
if (disabled) return;
|
|
1142
|
+
const filePath = join4(userConfigDir(home), STATE_FILENAME);
|
|
1143
|
+
const persisted = await loadState(filePath);
|
|
1144
|
+
if (persisted.savedAt === 0) return;
|
|
1145
|
+
const entries = Object.entries(persisted.learning).map(([namespace, usage]) => ({ namespace, usage }));
|
|
1146
|
+
const flaky = selectFlakyNamespaces(entries, 5);
|
|
1147
|
+
if (flaky.length === 0) return;
|
|
1148
|
+
print("RELIABILITY (dormant, <80% success)");
|
|
1149
|
+
const now = Date.now();
|
|
1150
|
+
for (const { namespace, usage } of flaky) {
|
|
1151
|
+
const rate = Math.round(usage.succeeded / usage.dispatched * 100);
|
|
1152
|
+
const age = formatRelativeAge(now - usage.lastUsedAt);
|
|
1153
|
+
print(` ${namespace} \u2014 ${usage.dispatched} calls, ${rate}% success, last used ${age} ago`);
|
|
1154
|
+
}
|
|
1155
|
+
print("");
|
|
1156
|
+
}
|
|
1078
1157
|
function formatRelativeAge(ms) {
|
|
1079
1158
|
const clamped = Math.max(0, ms);
|
|
1080
1159
|
const s = Math.floor(clamped / 1e3);
|
|
@@ -1712,9 +1791,61 @@ ${USAGE}` };
|
|
|
1712
1791
|
}
|
|
1713
1792
|
var INSTALL_USAGE = USAGE;
|
|
1714
1793
|
|
|
1794
|
+
// src/reset-learning-cmd.ts
|
|
1795
|
+
import { unlink } from "fs/promises";
|
|
1796
|
+
import { homedir as homedir6 } from "os";
|
|
1797
|
+
import { join as join6 } from "path";
|
|
1798
|
+
async function runResetLearning(opts = {}) {
|
|
1799
|
+
const home = opts.home ?? homedir6();
|
|
1800
|
+
const env = opts.env ?? process.env;
|
|
1801
|
+
const write = opts.out ?? ((s) => process.stdout.write(s));
|
|
1802
|
+
const writeErr = opts.err ?? ((s) => process.stderr.write(s));
|
|
1803
|
+
const lines = [];
|
|
1804
|
+
const print = (s = "") => {
|
|
1805
|
+
lines.push(s);
|
|
1806
|
+
write(`${s}
|
|
1807
|
+
`);
|
|
1808
|
+
};
|
|
1809
|
+
const printErr = (s) => {
|
|
1810
|
+
lines.push(s);
|
|
1811
|
+
writeErr(`${s}
|
|
1812
|
+
`);
|
|
1813
|
+
};
|
|
1814
|
+
const filePath = join6(userConfigDir(home), STATE_FILENAME);
|
|
1815
|
+
const raw = env.MCPH_DISABLE_PERSISTENCE;
|
|
1816
|
+
const disabled = raw !== void 0 && raw !== "" && (raw === "1" || raw.toLowerCase() === "true");
|
|
1817
|
+
if (disabled) {
|
|
1818
|
+
print("mcph reset-learning: persistence is disabled (MCPH_DISABLE_PERSISTENCE) \u2014 nothing to clear.");
|
|
1819
|
+
return { exitCode: 0, lines, removed: false, path: filePath };
|
|
1820
|
+
}
|
|
1821
|
+
const persisted = await loadState(filePath);
|
|
1822
|
+
const learningCount = Object.keys(persisted.learning).length;
|
|
1823
|
+
const packCount = persisted.packHistory.length;
|
|
1824
|
+
try {
|
|
1825
|
+
await unlink(filePath);
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
if (isFileNotFound2(err)) {
|
|
1828
|
+
print("mcph reset-learning: no persisted state to reset.");
|
|
1829
|
+
print(` path: ${filePath}`);
|
|
1830
|
+
return { exitCode: 0, lines, removed: false, path: filePath };
|
|
1831
|
+
}
|
|
1832
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1833
|
+
printErr(`mcph reset-learning: failed to remove ${filePath}: ${msg}`);
|
|
1834
|
+
return { exitCode: 1, lines, removed: false, path: filePath };
|
|
1835
|
+
}
|
|
1836
|
+
print("mcph reset-learning: cleared persisted state.");
|
|
1837
|
+
print(` path: ${filePath}`);
|
|
1838
|
+
print(` learning entries removed: ${learningCount}`);
|
|
1839
|
+
print(` pack history entries removed: ${packCount}`);
|
|
1840
|
+
return { exitCode: 0, lines, removed: true, path: filePath };
|
|
1841
|
+
}
|
|
1842
|
+
function isFileNotFound2(err) {
|
|
1843
|
+
return !!err && typeof err === "object" && "code" in err && err.code === "ENOENT";
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1715
1846
|
// src/server.ts
|
|
1716
1847
|
import { readFile as readFile6 } from "fs/promises";
|
|
1717
|
-
import { homedir as
|
|
1848
|
+
import { homedir as homedir7 } from "os";
|
|
1718
1849
|
import { isAbsolute, relative, resolve as resolve3 } from "path";
|
|
1719
1850
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1720
1851
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -3934,7 +4065,7 @@ function categorizeSpawnError(err) {
|
|
|
3934
4065
|
}
|
|
3935
4066
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
3936
4067
|
const client = new Client(
|
|
3937
|
-
{ name: "mcph", version: true ? "0.
|
|
4068
|
+
{ name: "mcph", version: true ? "0.38.0" : "dev" },
|
|
3938
4069
|
{ capabilities: {} }
|
|
3939
4070
|
);
|
|
3940
4071
|
let transport;
|
|
@@ -4332,51 +4463,6 @@ async function reportTools(serverId, tools) {
|
|
|
4332
4463
|
}
|
|
4333
4464
|
}
|
|
4334
4465
|
|
|
4335
|
-
// src/usage-hints.ts
|
|
4336
|
-
var MAX_PEERS = 3;
|
|
4337
|
-
var MIN_SUCCESS_TO_SHOW = 1;
|
|
4338
|
-
var RELIABILITY_MIN_OBSERVATIONS = 3;
|
|
4339
|
-
var RELIABILITY_THRESHOLD = 0.8;
|
|
4340
|
-
function buildCoUsageMap(packs) {
|
|
4341
|
-
const result = /* @__PURE__ */ new Map();
|
|
4342
|
-
for (const pack of packs) {
|
|
4343
|
-
for (const ns of pack.namespaces) {
|
|
4344
|
-
const bucket = result.get(ns) ?? /* @__PURE__ */ new Set();
|
|
4345
|
-
for (const peer of pack.namespaces) {
|
|
4346
|
-
if (peer !== ns) bucket.add(peer);
|
|
4347
|
-
}
|
|
4348
|
-
result.set(ns, bucket);
|
|
4349
|
-
}
|
|
4350
|
-
}
|
|
4351
|
-
const sorted = /* @__PURE__ */ new Map();
|
|
4352
|
-
for (const [ns, peers] of result) {
|
|
4353
|
-
sorted.set(ns, Array.from(peers).sort());
|
|
4354
|
-
}
|
|
4355
|
-
return sorted;
|
|
4356
|
-
}
|
|
4357
|
-
function formatUsageHint(usage, coUsedWith) {
|
|
4358
|
-
const parts = [];
|
|
4359
|
-
if (usage && usage.succeeded >= MIN_SUCCESS_TO_SHOW) {
|
|
4360
|
-
parts.push(`used ${usage.succeeded}x`);
|
|
4361
|
-
}
|
|
4362
|
-
if (coUsedWith.length > 0) {
|
|
4363
|
-
const shown = coUsedWith.slice(0, MAX_PEERS);
|
|
4364
|
-
const more = coUsedWith.length - shown.length;
|
|
4365
|
-
const names = shown.map((n) => `"${n}"`).join(", ");
|
|
4366
|
-
const tail = more > 0 ? ` +${more} more` : "";
|
|
4367
|
-
parts.push(`often loaded with ${names}${tail}`);
|
|
4368
|
-
}
|
|
4369
|
-
if (parts.length === 0) return null;
|
|
4370
|
-
return `usage: ${parts.join("; ")}`;
|
|
4371
|
-
}
|
|
4372
|
-
function formatReliabilityWarning(usage) {
|
|
4373
|
-
if (!usage || usage.dispatched < RELIABILITY_MIN_OBSERVATIONS) return null;
|
|
4374
|
-
const rate = usage.succeeded / usage.dispatched;
|
|
4375
|
-
if (rate >= RELIABILITY_THRESHOLD) return null;
|
|
4376
|
-
const pct = Math.round(rate * 100);
|
|
4377
|
-
return `reliability: ${pct}% success across ${usage.dispatched} past calls`;
|
|
4378
|
-
}
|
|
4379
|
-
|
|
4380
4466
|
// src/server.ts
|
|
4381
4467
|
var DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
4382
4468
|
function resolvePollIntervalMs() {
|
|
@@ -4460,7 +4546,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
4460
4546
|
this.apiUrl = apiUrl6;
|
|
4461
4547
|
this.token = token6;
|
|
4462
4548
|
this.server = new Server(
|
|
4463
|
-
{ name: "mcph", version: true ? "0.
|
|
4549
|
+
{ name: "mcph", version: true ? "0.38.0" : "dev" },
|
|
4464
4550
|
{
|
|
4465
4551
|
capabilities: {
|
|
4466
4552
|
tools: { listChanged: true },
|
|
@@ -5895,7 +5981,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
5895
5981
|
}
|
|
5896
5982
|
const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
|
|
5897
5983
|
try {
|
|
5898
|
-
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve3(
|
|
5984
|
+
const resolved = filepath.startsWith("~/") || filepath.startsWith("~\\") ? resolve3(homedir7(), filepath.slice(2)) : resolve3(filepath);
|
|
5899
5985
|
const resolvedBasename = resolved.split(/[/\\]/).pop() || "";
|
|
5900
5986
|
if (!ALLOWED_FILENAMES.includes(resolvedBasename)) {
|
|
5901
5987
|
return {
|
|
@@ -5912,7 +5998,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
5912
5998
|
const rel = relative(base, p);
|
|
5913
5999
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
5914
6000
|
};
|
|
5915
|
-
if (!isUnder(
|
|
6001
|
+
if (!isUnder(homedir7(), resolved) && !isUnder(process.cwd(), resolved)) {
|
|
5916
6002
|
return {
|
|
5917
6003
|
content: [
|
|
5918
6004
|
{ type: "text", text: "Import path must be under your home directory or the current working directory." }
|
|
@@ -6219,17 +6305,10 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
6219
6305
|
}
|
|
6220
6306
|
}
|
|
6221
6307
|
const now = Date.now();
|
|
6222
|
-
const flaky =
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
}).sort((a, b) => {
|
|
6227
|
-
const aRate = a.usage.succeeded / a.usage.dispatched;
|
|
6228
|
-
const bRate = b.usage.succeeded / b.usage.dispatched;
|
|
6229
|
-
if (aRate !== bRate) return aRate - bRate;
|
|
6230
|
-
if (a.usage.dispatched !== b.usage.dispatched) return b.usage.dispatched - a.usage.dispatched;
|
|
6231
|
-
return a.namespace.localeCompare(b.namespace);
|
|
6232
|
-
}).slice(0, 5);
|
|
6308
|
+
const flaky = selectFlakyNamespaces(
|
|
6309
|
+
this.learning.entries().filter(({ namespace }) => !this.connections.has(namespace)),
|
|
6310
|
+
5
|
|
6311
|
+
);
|
|
6233
6312
|
if (flaky.length > 0) {
|
|
6234
6313
|
lines.push("\nCross-session reliability (dormant, <80% success):");
|
|
6235
6314
|
for (const { namespace, usage } of flaky) {
|
|
@@ -6520,7 +6599,17 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
6520
6599
|
};
|
|
6521
6600
|
|
|
6522
6601
|
// src/index.ts
|
|
6523
|
-
var KNOWN_SUBCOMMANDS = [
|
|
6602
|
+
var KNOWN_SUBCOMMANDS = [
|
|
6603
|
+
"compliance",
|
|
6604
|
+
"install",
|
|
6605
|
+
"doctor",
|
|
6606
|
+
"reset-learning",
|
|
6607
|
+
"help",
|
|
6608
|
+
"--help",
|
|
6609
|
+
"-h",
|
|
6610
|
+
"--version",
|
|
6611
|
+
"-V"
|
|
6612
|
+
];
|
|
6524
6613
|
var subcommand = process.argv[2];
|
|
6525
6614
|
if (subcommand === "compliance") {
|
|
6526
6615
|
runComplianceCommand(process.argv.slice(3)).then((code) => process.exit(code));
|
|
@@ -6534,6 +6623,8 @@ if (subcommand === "compliance") {
|
|
|
6534
6623
|
runInstall(parsed.options).then((r) => process.exit(r.exitCode));
|
|
6535
6624
|
} else if (subcommand === "doctor") {
|
|
6536
6625
|
runDoctor().then((r) => process.exit(r.exitCode));
|
|
6626
|
+
} else if (subcommand === "reset-learning") {
|
|
6627
|
+
runResetLearning().then((r) => process.exit(r.exitCode));
|
|
6537
6628
|
} else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
6538
6629
|
const installBlock = ` ${INSTALL_USAGE.replace(/^Usage: /, "").replace(/\n/g, "\n ")}`;
|
|
6539
6630
|
process.stdout.write(
|
|
@@ -6545,6 +6636,7 @@ if (subcommand === "compliance") {
|
|
|
6545
6636
|
mcph install <client> [flags] Auto-edit an MCP client's config to launch mcph
|
|
6546
6637
|
mcph doctor Print loaded config + detected clients (support diagnostic)
|
|
6547
6638
|
mcph compliance <target> [flags] Run the compliance suite against an MCP server
|
|
6639
|
+
mcph reset-learning Clear cross-session learning history (~/.mcph/state.json)
|
|
6548
6640
|
mcph --version Print version
|
|
6549
6641
|
|
|
6550
6642
|
Install:
|
|
@@ -6565,7 +6657,7 @@ ${installBlock}
|
|
|
6565
6657
|
);
|
|
6566
6658
|
process.exit(0);
|
|
6567
6659
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
6568
|
-
process.stdout.write(`mcph ${true ? "0.
|
|
6660
|
+
process.stdout.write(`mcph ${true ? "0.38.0" : "dev"}
|
|
6569
6661
|
`);
|
|
6570
6662
|
process.exit(0);
|
|
6571
6663
|
} else if (subcommand && !subcommand.startsWith("-")) {
|
package/package.json
CHANGED