@levr-one/cli 0.11.6 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -773,6 +773,41 @@ function buildServerEntry(harness, mcpUrl, scope = defaultScope(harness), server
|
|
|
773
773
|
args
|
|
774
774
|
} };
|
|
775
775
|
}
|
|
776
|
+
/** The workspace a scoped MCP URL names (`…/w/<url_key>`), if any. */
|
|
777
|
+
function workspaceKeyOf(url) {
|
|
778
|
+
const m = /\/w\/([^/]+)$/.exec(url.replace(/\/+$/, ""));
|
|
779
|
+
if (!m?.[1]) return void 0;
|
|
780
|
+
try {
|
|
781
|
+
return decodeURIComponent(m[1]);
|
|
782
|
+
} catch {
|
|
783
|
+
return m[1];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* Allowed client entry names: 1-64 of `A-Za-z0-9_-`, and never starting with
|
|
788
|
+
* `-`. The name is passed as an argv element to the client's own CLI
|
|
789
|
+
* (`claude mcp add … <name> <url>`), so a leading `-` would be read there as a
|
|
790
|
+
* flag — the ENG-4159 argument-injection family.
|
|
791
|
+
*/
|
|
792
|
+
const ENTRY_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
793
|
+
function isValidEntryName(name) {
|
|
794
|
+
return ENTRY_NAME.test(name) && !name.startsWith("-");
|
|
795
|
+
}
|
|
796
|
+
/** Claude Code tool names are `mcp__<entry>__<tool>`; keep that well short. */
|
|
797
|
+
const BESIDE_NAME_MAX = 30;
|
|
798
|
+
/**
|
|
799
|
+
* The name for a second entry beside `serverName`: `<serverName>-<url_key>`,
|
|
800
|
+
* so it says which workspace it is. Undefined — and "Add beside it" is not
|
|
801
|
+
* offered — when the target URL names no workspace, or when that name would
|
|
802
|
+
* be too long or not a valid entry name. It is never shortened: a cut name
|
|
803
|
+
* could drop the workspace, or give two workspaces the same entry.
|
|
804
|
+
*/
|
|
805
|
+
function besideName(serverName, url) {
|
|
806
|
+
const key = workspaceKeyOf(url);
|
|
807
|
+
if (!key) return void 0;
|
|
808
|
+
const name = `${serverName}-${key}`;
|
|
809
|
+
return name.length <= BESIDE_NAME_MAX && isValidEntryName(name) ? name : void 0;
|
|
810
|
+
}
|
|
776
811
|
|
|
777
812
|
//#endregion
|
|
778
813
|
//#region ../mcp-harnesses/dist/node/paths.js
|
|
@@ -1611,47 +1646,44 @@ function needsShell(binPath) {
|
|
|
1611
1646
|
*/
|
|
1612
1647
|
const SHELL_METACHARACTERS = /[&|<>^"%!`]/;
|
|
1613
1648
|
/**
|
|
1614
|
-
*
|
|
1649
|
+
* Everything decided before a command is spawned, shared by both runners: an
|
|
1650
|
+
* outcome when it must not run, else how to run it.
|
|
1615
1651
|
*
|
|
1616
1652
|
* Resolution uses {@link whichPathSync} — a pure `PATH` scan that spawns
|
|
1617
1653
|
* nothing, so the "is this installed" check can never itself be the thing
|
|
1618
1654
|
* that hangs. It returns the resolved path rather than a boolean because the
|
|
1619
1655
|
* extension decides how the command must be launched.
|
|
1620
1656
|
*/
|
|
1621
|
-
function
|
|
1657
|
+
function prepare(argv, opts) {
|
|
1622
1658
|
const env = opts.env ?? defaultEnv();
|
|
1623
1659
|
const dryRun = opts.dryRun ?? false;
|
|
1624
1660
|
const [bin, ...args] = argv;
|
|
1625
|
-
if (!bin) return {
|
|
1661
|
+
if (!bin) return { skip: {
|
|
1626
1662
|
executed: false,
|
|
1627
1663
|
ok: false,
|
|
1628
1664
|
reason: "empty-command"
|
|
1629
|
-
};
|
|
1630
|
-
if (dryRun) return {
|
|
1665
|
+
} };
|
|
1666
|
+
if (dryRun) return { skip: {
|
|
1631
1667
|
executed: false,
|
|
1632
1668
|
ok: true,
|
|
1633
1669
|
reason: "dry-run"
|
|
1634
|
-
};
|
|
1670
|
+
} };
|
|
1635
1671
|
const binPath = whichPathSync(bin, env);
|
|
1636
|
-
if (!binPath) return {
|
|
1672
|
+
if (!binPath) return { skip: {
|
|
1637
1673
|
executed: false,
|
|
1638
1674
|
ok: true,
|
|
1639
1675
|
reason: "binary-not-found"
|
|
1640
|
-
};
|
|
1676
|
+
} };
|
|
1641
1677
|
const useShell = needsShell(binPath);
|
|
1642
|
-
if (useShell && args.some((a) => SHELL_METACHARACTERS.test(a))) return {
|
|
1678
|
+
if (useShell && (binPath.includes("\"") || args.some((a) => SHELL_METACHARACTERS.test(a)))) return { skip: {
|
|
1643
1679
|
executed: false,
|
|
1644
1680
|
ok: false,
|
|
1645
1681
|
reason: "unsafe-argument"
|
|
1646
|
-
};
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
"ignore",
|
|
1652
|
-
"pipe",
|
|
1653
|
-
"pipe"
|
|
1654
|
-
],
|
|
1682
|
+
} };
|
|
1683
|
+
return { spawn: {
|
|
1684
|
+
binPath: useShell ? `"${binPath}"` : binPath,
|
|
1685
|
+
args,
|
|
1686
|
+
options: {
|
|
1655
1687
|
timeout: COMMAND_TIMEOUT_MS,
|
|
1656
1688
|
env: {
|
|
1657
1689
|
...childVars(env),
|
|
@@ -1660,20 +1692,42 @@ function runHarnessCommandSync(argv, opts = {}) {
|
|
|
1660
1692
|
},
|
|
1661
1693
|
cwd: env.cwd,
|
|
1662
1694
|
...useShell ? { shell: true } : {}
|
|
1695
|
+
}
|
|
1696
|
+
} };
|
|
1697
|
+
}
|
|
1698
|
+
/** A command that ran and failed (or could not start), as an outcome. */
|
|
1699
|
+
function failureOutcome(err) {
|
|
1700
|
+
const e = err;
|
|
1701
|
+
const exitCode = typeof e.status === "number" ? e.status : typeof e.code === "number" ? e.code : void 0;
|
|
1702
|
+
const raw = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf8") ?? "";
|
|
1703
|
+
return {
|
|
1704
|
+
executed: true,
|
|
1705
|
+
ok: false,
|
|
1706
|
+
...exitCode === void 0 ? {} : { exitCode },
|
|
1707
|
+
stderr: (raw.trim() || e.message || "command failed").slice(0, MAX_STDERR)
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
/** Run a harness CLI invocation, or explain why it was not run. */
|
|
1711
|
+
function runHarnessCommandSync(argv, opts = {}) {
|
|
1712
|
+
const prepared = prepare(argv, opts);
|
|
1713
|
+
if ("skip" in prepared) return prepared.skip;
|
|
1714
|
+
const { binPath, args, options } = prepared.spawn;
|
|
1715
|
+
try {
|
|
1716
|
+
execFileSync(binPath, args, {
|
|
1717
|
+
...options,
|
|
1718
|
+
encoding: "utf8",
|
|
1719
|
+
stdio: [
|
|
1720
|
+
"ignore",
|
|
1721
|
+
"pipe",
|
|
1722
|
+
"pipe"
|
|
1723
|
+
]
|
|
1663
1724
|
});
|
|
1664
1725
|
return {
|
|
1665
1726
|
executed: true,
|
|
1666
1727
|
ok: true
|
|
1667
1728
|
};
|
|
1668
1729
|
} catch (err) {
|
|
1669
|
-
|
|
1670
|
-
const raw = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString("utf8") ?? "";
|
|
1671
|
-
return {
|
|
1672
|
-
executed: true,
|
|
1673
|
-
ok: false,
|
|
1674
|
-
...typeof e.status === "number" ? { exitCode: e.status } : {},
|
|
1675
|
-
stderr: (raw.trim() || e.message || "command failed").slice(0, MAX_STDERR)
|
|
1676
|
-
};
|
|
1730
|
+
return failureOutcome(err);
|
|
1677
1731
|
}
|
|
1678
1732
|
}
|
|
1679
1733
|
|
|
@@ -1745,8 +1799,16 @@ function canonical(value) {
|
|
|
1745
1799
|
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([x], [y]) => x < y ? -1 : x > y ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`).join(",")}}`;
|
|
1746
1800
|
return JSON.stringify(value) ?? "undefined";
|
|
1747
1801
|
}
|
|
1802
|
+
function drive(steps, run) {
|
|
1803
|
+
let step = steps.next();
|
|
1804
|
+
while (!step.done) step = steps.next(run(step.value.argv, step.value.opts));
|
|
1805
|
+
return step.value;
|
|
1806
|
+
}
|
|
1748
1807
|
/** Merge (or preview) our MCP into a harness config. */
|
|
1749
1808
|
function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
1809
|
+
return drive(installSteps(harness, mcpUrl, opts), opts.runCommand ?? runHarnessCommandSync);
|
|
1810
|
+
}
|
|
1811
|
+
function* installSteps(harness, mcpUrl, opts) {
|
|
1750
1812
|
const env = opts.env ?? defaultEnv();
|
|
1751
1813
|
const dryRun = opts.dryRun ?? false;
|
|
1752
1814
|
const scope = opts.scope ?? defaultScope(harness);
|
|
@@ -1785,13 +1847,12 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
1785
1847
|
alreadyConfigured: false,
|
|
1786
1848
|
reason: "unrecognized-entry"
|
|
1787
1849
|
};
|
|
1788
|
-
if (opts.replaceExisting) return
|
|
1850
|
+
if (opts.replaceExisting) return yield* replaceSteps(harness, argv, existing$1.url, {
|
|
1789
1851
|
env,
|
|
1790
1852
|
dryRun,
|
|
1791
1853
|
scope,
|
|
1792
1854
|
serverName,
|
|
1793
|
-
execute: opts.execute ?? true
|
|
1794
|
-
run: opts.runCommand ?? runHarnessCommandSync
|
|
1855
|
+
execute: opts.execute ?? true
|
|
1795
1856
|
});
|
|
1796
1857
|
return {
|
|
1797
1858
|
...base,
|
|
@@ -1802,11 +1863,13 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
1802
1863
|
...existing$1.url ? { currentUrl: existing$1.url } : {}
|
|
1803
1864
|
};
|
|
1804
1865
|
}
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1866
|
+
const outcome = opts.execute ?? true ? yield {
|
|
1867
|
+
argv,
|
|
1868
|
+
opts: {
|
|
1869
|
+
env,
|
|
1870
|
+
dryRun
|
|
1871
|
+
}
|
|
1872
|
+
} : {
|
|
1810
1873
|
executed: false,
|
|
1811
1874
|
ok: true,
|
|
1812
1875
|
reason: "not-requested"
|
|
@@ -1815,7 +1878,7 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
1815
1878
|
...base,
|
|
1816
1879
|
ok: outcome.ok,
|
|
1817
1880
|
executed: outcome.executed,
|
|
1818
|
-
...outcome.ok ? {} : { commandError: outcome
|
|
1881
|
+
...outcome.ok ? {} : { commandError: notRunError(outcome) },
|
|
1819
1882
|
alreadyConfigured: false
|
|
1820
1883
|
};
|
|
1821
1884
|
}
|
|
@@ -1907,7 +1970,14 @@ function installHarnessSync(harness, mcpUrl, opts = {}) {
|
|
|
1907
1970
|
* second command must not leave the user with no entry at all without saying
|
|
1908
1971
|
* so.
|
|
1909
1972
|
*/
|
|
1910
|
-
|
|
1973
|
+
/**
|
|
1974
|
+
* What went wrong with a failed command: its stderr, else why it was never
|
|
1975
|
+
* run — so a refusal never reads as "unknown reason" (ENG-6737).
|
|
1976
|
+
*/
|
|
1977
|
+
function notRunError(outcome) {
|
|
1978
|
+
return outcome.stderr ?? (outcome.reason ? `not run: ${outcome.reason}` : void 0);
|
|
1979
|
+
}
|
|
1980
|
+
function* replaceSteps(harness, addArgv, previousUrl, ctx) {
|
|
1911
1981
|
const removeArgv = removeCommandArgv(ctx.scope, ctx.serverName);
|
|
1912
1982
|
const base = {
|
|
1913
1983
|
wrote: false,
|
|
@@ -1923,11 +1993,15 @@ function replaceViaCommand(harness, addArgv, previousUrl, ctx) {
|
|
|
1923
1993
|
ok: true,
|
|
1924
1994
|
executed: false
|
|
1925
1995
|
};
|
|
1926
|
-
const removed =
|
|
1996
|
+
const removed = yield {
|
|
1997
|
+
argv: removeArgv,
|
|
1998
|
+
opts: { env: ctx.env }
|
|
1999
|
+
};
|
|
1927
2000
|
if (!removed.executed) return {
|
|
1928
2001
|
...base,
|
|
1929
2002
|
ok: removed.ok,
|
|
1930
|
-
executed: false
|
|
2003
|
+
executed: false,
|
|
2004
|
+
...removed.ok ? {} : { commandError: notRunError(removed) }
|
|
1931
2005
|
};
|
|
1932
2006
|
if (!removed.ok) return {
|
|
1933
2007
|
...base,
|
|
@@ -1935,21 +2009,26 @@ function replaceViaCommand(harness, addArgv, previousUrl, ctx) {
|
|
|
1935
2009
|
executed: true,
|
|
1936
2010
|
commandError: `could not remove the existing entry: ${removed.stderr ?? "unknown error"}`
|
|
1937
2011
|
};
|
|
1938
|
-
const added =
|
|
2012
|
+
const added = yield {
|
|
2013
|
+
argv: addArgv,
|
|
2014
|
+
opts: { env: ctx.env }
|
|
2015
|
+
};
|
|
1939
2016
|
if (added.ok) return {
|
|
1940
2017
|
...base,
|
|
1941
2018
|
ok: true,
|
|
1942
2019
|
executed: true
|
|
1943
2020
|
};
|
|
1944
|
-
const
|
|
1945
|
-
|
|
2021
|
+
const restore = yield {
|
|
2022
|
+
argv: renderHarnessCommand(harness, previousUrl, ctx.scope, ctx.serverName),
|
|
2023
|
+
opts: { env: ctx.env }
|
|
2024
|
+
};
|
|
1946
2025
|
return {
|
|
1947
2026
|
...base,
|
|
1948
2027
|
ok: false,
|
|
1949
2028
|
executed: true,
|
|
1950
|
-
commandError: added
|
|
2029
|
+
commandError: notRunError(added) ?? "unknown error",
|
|
1951
2030
|
restored: restore.ok && restore.executed,
|
|
1952
|
-
...restore.ok && restore.executed ? {} : { restoreError: restore
|
|
2031
|
+
...restore.ok && restore.executed ? {} : { restoreError: notRunError(restore) ?? "the restore command did not run" }
|
|
1953
2032
|
};
|
|
1954
2033
|
}
|
|
1955
2034
|
/**
|
|
@@ -2111,16 +2190,6 @@ function servedBySessionApi(mcpUrl) {
|
|
|
2111
2190
|
}
|
|
2112
2191
|
/** `…/v1/mcp/w/<url_key>`: the URL already names one workspace (ENG-6300). */
|
|
2113
2192
|
const SCOPED_SUFFIX = /\/w\/[^/]+$/;
|
|
2114
|
-
/** The workspace a scoped MCP URL names (`…/w/<url_key>`), if any. */
|
|
2115
|
-
function workspaceKeyOf(url) {
|
|
2116
|
-
const m = /\/w\/([^/]+)$/.exec(stripSlash(url));
|
|
2117
|
-
if (!m?.[1]) return void 0;
|
|
2118
|
-
try {
|
|
2119
|
-
return decodeURIComponent(m[1]);
|
|
2120
|
-
} catch {
|
|
2121
|
-
return m[1];
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
2193
|
/** Does this MCP URL already name a workspace? */
|
|
2125
2194
|
function isScopedMcpUrl(url) {
|
|
2126
2195
|
return SCOPED_SUFFIX.test(stripSlash(url));
|
|
@@ -2188,16 +2257,6 @@ function entryState(s, url) {
|
|
|
2188
2257
|
return s.currentUrl === void 0 ? "unrecognized" : "other-url";
|
|
2189
2258
|
}
|
|
2190
2259
|
/**
|
|
2191
|
-
* Allowed client entry names: 1-64 of `A-Za-z0-9_-`, and never starting with
|
|
2192
|
-
* `-`. The name is passed as an argv element to the client's own CLI
|
|
2193
|
-
* (`claude mcp add … <name> <url>`), so a leading `-` would be read there as a
|
|
2194
|
-
* flag — the ENG-4159 argument-injection family.
|
|
2195
|
-
*/
|
|
2196
|
-
const ENTRY_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2197
|
-
function isValidEntryName(name) {
|
|
2198
|
-
return ENTRY_NAME.test(name) && !name.startsWith("-");
|
|
2199
|
-
}
|
|
2200
|
-
/**
|
|
2201
2260
|
* The scope an install would land in: the requested one, or, for a harness
|
|
2202
2261
|
* that cannot do it, its own default, which is where the fallback goes.
|
|
2203
2262
|
*/
|
|
@@ -2261,21 +2320,6 @@ function clientChoices(detected, scope, url, serverName = DEFAULT_SERVER_NAME) {
|
|
|
2261
2320
|
};
|
|
2262
2321
|
});
|
|
2263
2322
|
}
|
|
2264
|
-
/** Claude Code tool names are `mcp__<entry>__<tool>`; keep that well short. */
|
|
2265
|
-
const BESIDE_NAME_MAX = 30;
|
|
2266
|
-
/**
|
|
2267
|
-
* The name for a second entry beside `serverName`: `<serverName>-<url_key>`,
|
|
2268
|
-
* so it says which workspace it is. Undefined — and "Add beside it" is not
|
|
2269
|
-
* offered — when the target URL names no workspace, or when that name would
|
|
2270
|
-
* be too long or not a valid entry name. It is never shortened: a cut name
|
|
2271
|
-
* could drop the workspace, or give two workspaces the same entry.
|
|
2272
|
-
*/
|
|
2273
|
-
function besideName(serverName, url) {
|
|
2274
|
-
const key = workspaceKeyOf(url);
|
|
2275
|
-
if (!key) return void 0;
|
|
2276
|
-
const name = `${serverName}-${key}`;
|
|
2277
|
-
return name.length <= BESIDE_NAME_MAX && isValidEntryName(name) ? name : void 0;
|
|
2278
|
-
}
|
|
2279
2323
|
/**
|
|
2280
2324
|
* The selected clients whose entry is not this URL's: each needs a decision —
|
|
2281
2325
|
* switch it, add beside it, or leave it — before anything is installed
|
package/dist/cli.js
CHANGED
|
@@ -209,7 +209,7 @@ Examples:
|
|
|
209
209
|
aliases: { y: "yes" }
|
|
210
210
|
},
|
|
211
211
|
loader: async () => {
|
|
212
|
-
const { mcpAddHandler } = await import("./addHandler-
|
|
212
|
+
const { mcpAddHandler } = await import("./addHandler-BqLLDeKp.js");
|
|
213
213
|
return mcpAddHandler;
|
|
214
214
|
}
|
|
215
215
|
});
|
|
@@ -876,7 +876,7 @@ Examples:
|
|
|
876
876
|
|
|
877
877
|
//#endregion
|
|
878
878
|
//#region package.json
|
|
879
|
-
var version = "0.11.
|
|
879
|
+
var version = "0.11.7";
|
|
880
880
|
|
|
881
881
|
//#endregion
|
|
882
882
|
//#region src/app.ts
|