From 2cd8db0c534e4ffeaa6ab56bbfd0e1d847ce56c9 Mon Sep 17 00:00:00 2001 From: Henry Heng Date: Fri, 6 Jun 2025 18:07:43 +0100 Subject: [PATCH] Bugfix/Generator Prompt Markdown Format (#4592) * add ui fixes * refactor: update image structure in CustomAssistantLayout to use object format --- packages/ui/package.json | 1 + .../dialog/AgentflowGeneratorDialog.jsx | 22 +++++-- .../ui-component/dialog/NodeInfoDialog.jsx | 6 +- .../ui-component/extended/SpeechToText.jsx | 6 +- .../CustomAssistantConfigurePreview.jsx | 40 ++++++++++-- .../custom/CustomAssistantLayout.jsx | 4 +- .../ui/src/views/canvas/NodeInputHandler.jsx | 15 ++++- .../credentials/CredentialListDialog.jsx | 6 +- .../views/docstore/ComponentsListDialog.jsx | 6 +- .../views/docstore/DocStoreInputHandler.jsx | 26 ++++++-- .../docstore/DocumentLoaderListDialog.jsx | 6 +- .../docstore/LoaderConfigPreviewChunks.jsx | 36 +++++++++-- .../views/docstore/VectorStoreConfigure.jsx | 64 ++++++++++++++----- .../src/views/docstore/VectorStoreQuery.jsx | 16 ++++- pnpm-lock.yaml | 32 +++++++++- 15 files changed, 234 insertions(+), 52 deletions(-) diff --git a/packages/ui/package.json b/packages/ui/package.json index f3ed1bbda..44af1c6a7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -68,6 +68,7 @@ "rehype-raw": "^7.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", + "showdown": "^2.1.0", "tippy.js": "^6.3.7", "uuid": "^9.0.1", "yup": "^0.32.9" diff --git a/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx b/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx index 2819e2e62..425c65aa9 100644 --- a/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx +++ b/packages/ui/src/ui-component/dialog/AgentflowGeneratorDialog.jsx @@ -15,7 +15,7 @@ import { Dropdown } from '@/ui-component/dropdown/Dropdown' import { useTheme } from '@mui/material/styles' import assistantsApi from '@/api/assistants' import { baseURL } from '@/store/constant' -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler' import useApi from '@/hooks/useApi' @@ -55,6 +55,15 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) => const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const handleChatModelDataChange = ({ inputParam, newValue }) => { + setSelectedChatModel((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + useEffect(() => { if (getChatModelsApi.data) { setChatModelsComponents(getChatModelsApi.data) @@ -303,10 +312,15 @@ const AgentflowGeneratorDialog = ({ show, dialogProps, onCancel, onConfirm }) => borderRadius: 2 }} > - {(selectedChatModel.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + {showHideInputParams(selectedChatModel) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( - + ))} )} diff --git a/packages/ui/src/ui-component/dialog/NodeInfoDialog.jsx b/packages/ui/src/ui-component/dialog/NodeInfoDialog.jsx index 58affea28..2b640196d 100644 --- a/packages/ui/src/ui-component/dialog/NodeInfoDialog.jsx +++ b/packages/ui/src/ui-component/dialog/NodeInfoDialog.jsx @@ -81,7 +81,11 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => { height: 50, marginRight: 10, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > { width: 50, height: 50, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > { const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const handleChatModelDataChange = ({ inputParam, newValue }) => { + setSelectedChatModel((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleToolDataChange = + (toolIndex) => + ({ inputParam, newValue }) => { + setSelectedTools((prevTools) => { + const updatedTools = [...prevTools] + const updatedTool = { ...updatedTools[toolIndex] } + updatedTool.inputs[inputParam.name] = newValue + updatedTool.inputParams = showHideInputParams(updatedTool) + updatedTools[toolIndex] = updatedTool + return updatedTools + }) + } + const displayWarning = () => { enqueueSnackbar({ message: 'Please fill in all mandatory fields.', @@ -1126,13 +1148,14 @@ const CustomAssistantConfigurePreview = () => { borderRadius: 2 }} > - {(selectedChatModel.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + {showHideInputParams(selectedChatModel) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( ))} @@ -1217,13 +1240,16 @@ const CustomAssistantConfigurePreview = () => { mb: 1 }} > - {(tool.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) - .map((inputParam, index) => ( + {showHideInputParams(tool) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) + .map((inputParam, inputIndex) => ( ))} diff --git a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx index fcdb4f2b8..6597bfeb6 100644 --- a/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx +++ b/packages/ui/src/views/assistants/custom/CustomAssistantLayout.jsx @@ -64,7 +64,9 @@ const CustomAssistantLayout = () => { const getImages = (details) => { const images = [] if (details && details.chatModel && details.chatModel.name) { - images.push(`${baseURL}/api/v1/node-icon/${details.chatModel.name}`) + images.push({ + imageSrc: `${baseURL}/api/v1/node-icon/${details.chatModel.name}` + }) } return images } diff --git a/packages/ui/src/views/canvas/NodeInputHandler.jsx b/packages/ui/src/views/canvas/NodeInputHandler.jsx index df22995c6..a3c963d1a 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.jsx +++ b/packages/ui/src/views/canvas/NodeInputHandler.jsx @@ -3,6 +3,7 @@ import { Handle, Position, useUpdateNodeInternals } from 'reactflow' import { useEffect, useRef, useState, useContext } from 'react' import { useSelector, useDispatch } from 'react-redux' import { cloneDeep } from 'lodash' +import showdown from 'showdown' // material-ui import { useTheme, styled } from '@mui/material/styles' @@ -98,6 +99,13 @@ const StyledPopper = styled(Popper)({ } }) +const markdownConverter = new showdown.Converter({ + simplifiedAutoLink: true, + strikethrough: true, + tables: true, + tasklists: true +}) + // ===========================|| NodeInputHandler ||=========================== // const NodeInputHandler = ({ @@ -1389,7 +1397,12 @@ const NodeInputHandler = ({ onCancel={() => setPromptGeneratorDialogOpen(false)} onConfirm={(generatedInstruction) => { try { - data.inputs[inputParam.name] = generatedInstruction + if (inputParam?.acceptVariable && window.location.href.includes('v2/agentcanvas')) { + const htmlContent = markdownConverter.makeHtml(generatedInstruction) + data.inputs[inputParam.name] = htmlContent + } else { + data.inputs[inputParam.name] = generatedInstruction + } setPromptGeneratorDialogOpen(false) } catch (error) { enqueueSnackbar({ diff --git a/packages/ui/src/views/credentials/CredentialListDialog.jsx b/packages/ui/src/views/credentials/CredentialListDialog.jsx index d5d1058d2..edf5d6e23 100644 --- a/packages/ui/src/views/credentials/CredentialListDialog.jsx +++ b/packages/ui/src/views/credentials/CredentialListDialog.jsx @@ -140,7 +140,11 @@ const CredentialListDialog = ({ show, dialogProps, onCancel, onCredentialSelecte width: 50, height: 50, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > { +const DocStoreInputHandler = ({ inputParam, data, disabled = false, onNodeDataChange }) => { const customization = useSelector((state) => state.customization) + const flowContextValue = useContext(flowContext) + const nodeDataChangeHandler = onNodeDataChange || flowContextValue?.onNodeDataChange const [showExpandDialog, setShowExpandDialog] = useState(false) const [expandDialogProps, setExpandDialogProps] = useState({}) @@ -35,6 +38,14 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({}) const [reloadTimestamp, setReloadTimestamp] = useState(Date.now().toString()) + const handleDataChange = ({ inputParam, newValue }) => { + data.inputs[inputParam.name] = newValue + const allowedShowHideInputTypes = ['boolean', 'asyncOptions', 'asyncMultiOptions', 'options', 'multiOptions'] + if (allowedShowHideInputTypes.includes(inputParam.type) && nodeDataChangeHandler) { + nodeDataChangeHandler({ nodeId: data.id, inputParam, newValue }) + } + } + const onExpandDialogClicked = (value, inputParam) => { const dialogProps = { value, @@ -149,7 +160,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { {inputParam.type === 'boolean' && ( (data.inputs[inputParam.name] = newValue)} + onChange={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? false} /> )} @@ -203,7 +214,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { disabled={disabled} name={inputParam.name} options={inputParam.options} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} /> )} @@ -213,7 +224,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { disabled={disabled} name={inputParam.name} options={inputParam.options} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} /> )} @@ -230,7 +241,7 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { freeSolo={inputParam.freeSolo} multiple={inputParam.type === 'asyncMultiOptions'} value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'} - onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)} + onSelect={(newValue) => handleDataChange({ inputParam, newValue })} onCreateNew={() => addAsyncOption(inputParam.name)} /> @@ -296,7 +307,8 @@ const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => { DocStoreInputHandler.propTypes = { inputParam: PropTypes.object, data: PropTypes.object, - disabled: PropTypes.bool + disabled: PropTypes.bool, + onNodeDataChange: PropTypes.func } export default DocStoreInputHandler diff --git a/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx b/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx index a8722ed35..8a1caa38d 100644 --- a/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx +++ b/packages/ui/src/views/docstore/DocumentLoaderListDialog.jsx @@ -153,7 +153,11 @@ const DocumentLoaderListDialog = ({ show, dialogProps, onCancel, onDocLoaderSele width: 50, height: 50, borderRadius: '50%', - backgroundColor: 'white' + backgroundColor: 'white', + flexShrink: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center' }} > ({ @@ -98,6 +98,24 @@ const LoaderConfigPreviewChunks = () => { const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) + const handleDocumentLoaderDataChange = ({ inputParam, newValue }) => { + setSelectedDocumentLoader((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleTextSplitterDataChange = ({ inputParam, newValue }) => { + setSelectedTextSplitter((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const onSplitterChange = (name) => { const textSplitter = (textSplitterNodes ?? []).find((splitter) => splitter.name === name) if (textSplitter) { @@ -452,13 +470,14 @@ const LoaderConfigPreviewChunks = () => { {selectedDocumentLoader && Object.keys(selectedDocumentLoader).length > 0 && - (selectedDocumentLoader.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedDocumentLoader) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( ))} {textSplitterNodes && textSplitterNodes.length > 0 && ( @@ -511,10 +530,15 @@ const LoaderConfigPreviewChunks = () => { )} {Object.keys(selectedTextSplitter).length > 0 && - (selectedTextSplitter.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedTextSplitter) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( - + ))} diff --git a/packages/ui/src/views/docstore/VectorStoreConfigure.jsx b/packages/ui/src/views/docstore/VectorStoreConfigure.jsx index e0cbcc4f7..f05759a8e 100644 --- a/packages/ui/src/views/docstore/VectorStoreConfigure.jsx +++ b/packages/ui/src/views/docstore/VectorStoreConfigure.jsx @@ -40,7 +40,7 @@ import Storage from '@mui/icons-material/Storage' import DynamicFeed from '@mui/icons-material/Filter1' // utils -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import useNotifier from '@/utils/useNotifier' // const @@ -89,6 +89,33 @@ const VectorStoreConfigure = () => { const [showUpsertHistoryDetailsDialog, setShowUpsertHistoryDetailsDialog] = useState(false) const [upsertDetailsDialogProps, setUpsertDetailsDialogProps] = useState({}) + const handleEmbeddingsProviderDataChange = ({ inputParam, newValue }) => { + setSelectedEmbeddingsProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => { + setSelectedVectorStoreProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + + const handleRecordManagerProviderDataChange = ({ inputParam, newValue }) => { + setSelectedRecordManagerProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const onEmbeddingsSelected = (component) => { const nodeData = cloneDeep(initNode(component, uuidv4())) if (!showEmbeddingsListDialog && documentStore.embeddingConfig) { @@ -599,14 +626,17 @@ const VectorStoreConfigure = () => { {selectedEmbeddingsProvider && Object.keys(selectedEmbeddingsProvider).length > 0 && - (selectedEmbeddingsProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedEmbeddingsProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( ))} @@ -714,14 +744,17 @@ const VectorStoreConfigure = () => { {selectedVectorStoreProvider && Object.keys(selectedVectorStoreProvider).length > 0 && - (selectedVectorStoreProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedVectorStoreProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( ))} @@ -837,17 +870,18 @@ const VectorStoreConfigure = () => { {selectedRecordManagerProvider && Object.keys(selectedRecordManagerProvider).length > 0 && - (selectedRecordManagerProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedRecordManagerProvider) + .filter( + (inputParam) => !inputParam.hidden && inputParam.display !== false + ) .map((inputParam, index) => ( - <> - - + ))} diff --git a/packages/ui/src/views/docstore/VectorStoreQuery.jsx b/packages/ui/src/views/docstore/VectorStoreQuery.jsx index 6e0fd1b0e..f0650ae41 100644 --- a/packages/ui/src/views/docstore/VectorStoreQuery.jsx +++ b/packages/ui/src/views/docstore/VectorStoreQuery.jsx @@ -31,7 +31,7 @@ import useApi from '@/hooks/useApi' import { useAuth } from '@/hooks/useAuth' import useNotifier from '@/utils/useNotifier' import { baseURL } from '@/store/constant' -import { initNode } from '@/utils/genericHelper' +import { initNode, showHideInputParams } from '@/utils/genericHelper' import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' const CardWrapper = styled(MainCard)(({ theme }) => ({ @@ -84,6 +84,15 @@ const VectorStoreQuery = () => { const getVectorStoreNodeDetailsApi = useApi(nodesApi.getSpecificNode) const [selectedVectorStoreProvider, setSelectedVectorStoreProvider] = useState({}) + const handleVectorStoreProviderDataChange = ({ inputParam, newValue }) => { + setSelectedVectorStoreProvider((prevData) => { + const updatedData = { ...prevData } + updatedData.inputs[inputParam.name] = newValue + updatedData.inputParams = showHideInputParams(updatedData) + return updatedData + }) + } + const chunkSelected = (chunkId, selectedChunkNumber) => { const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId) const dialogProps = { @@ -354,14 +363,15 @@ const VectorStoreQuery = () => { {selectedVectorStoreProvider && Object.keys(selectedVectorStoreProvider).length > 0 && - (selectedVectorStoreProvider.inputParams ?? []) - .filter((inputParam) => !inputParam.hidden) + showHideInputParams(selectedVectorStoreProvider) + .filter((inputParam) => !inputParam.hidden && inputParam.display !== false) .map((inputParam, index) => ( ))} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ab10f735..7dc0a2897 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -726,7 +726,7 @@ importers: version: 6.9.15 openai: specifier: 4.96.0 - version: 4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) + version: 4.96.0(encoding@0.1.13)(ws@8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) passport: specifier: ^0.7.0 version: 0.7.0 @@ -1067,6 +1067,9 @@ importers: remark-math: specifier: ^5.1.1 version: 5.1.1 + showdown: + specifier: ^2.1.0 + version: 2.1.0 tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -16119,6 +16122,10 @@ packages: shimmer@1.2.1: resolution: { integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== } + showdown@2.1.0: + resolution: { integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== } + hasBin: true + shx@0.3.4: resolution: { integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== } engines: { node: '>=6' } @@ -21653,7 +21660,7 @@ snapshots: dotenv: 16.4.5 openai: 4.96.0(encoding@0.1.13)(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4) sharp: 0.33.5 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) zod: 3.22.4 zod-to-json-schema: 3.24.1(zod@3.22.4) transitivePeerDependencies: @@ -22402,7 +22409,7 @@ snapshots: '@google/genai@0.7.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)': dependencies: google-auth-library: 9.15.1(encoding@0.1.13) - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4) + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) transitivePeerDependencies: - bufferutil - encoding @@ -36019,6 +36026,21 @@ snapshots: transitivePeerDependencies: - encoding + openai@4.96.0(encoding@0.1.13)(ws@8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4))(zod@3.22.4): + dependencies: + '@types/node': 18.19.23 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0(encoding@0.1.13) + optionalDependencies: + ws: 8.18.2(bufferutil@4.0.8)(utf-8-validate@6.0.4) + zod: 3.22.4 + transitivePeerDependencies: + - encoding + openapi-types@12.1.3: {} openapi-typescript-fetch@1.1.3: {} @@ -38734,6 +38756,10 @@ snapshots: shimmer@1.2.1: {} + showdown@2.1.0: + dependencies: + commander: 9.5.0 + shx@0.3.4: dependencies: minimist: 1.2.8