diff --git a/.gitignore b/.gitignore index 76add87..77436d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules -dist \ No newline at end of file +dist +.vscode/* +example/.vscode/launch.json +*.map \ No newline at end of file diff --git a/README.md b/README.md index 6685e2c..34cf80a 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,125 @@ A common request from companies and organizations considering bots is the ability to "hand off" a customer from a bot to a human agent, as seamlessly as possible. -This project implements an unopinionated core framework called **Handoff** which enables bot authors to implement a wide variety of scenarios, including a full-fledged call center app, with minimal changes to the actual bot. +This project implements a framework called **Handoff** which enables bot authors to implement a wide variety of scenarios, including a full-fledged call center app, with minimal changes to the actual bot. It also includes a very simple implementation that illustrates the core concepts with minimal configuration. This project is in heavy flux, but is now in a usable state. However this should still be considered a sample, and not an officially supported Microsoft product. -This project is written in TypeScript. follow the instructions to compile the code before running or deploying. +This project is written in TypeScript. -There is also an example of [handoff to human using the C# SDK](https://github.com/tompaana/intermediator-bot-sample) +[Source Code](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff) -## Conceptual Overview +See [example folder](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff/example) for a full bot example. -This framework connects *Customers* and *Agents*. +## Basic Usage -### Conversation State +```javascript +// Imports +const express = require('express'); +const builder = require('botbuilder'); +const handoff = require('botbuilder-handoff'); -Each customer conversation is in one of the following states: +// Setup Express Server (N.B: If you are already using restify for your bot, you will need replace it with an express server) +const app = express(); +app.listen(process.env.port || process.env.PORT || 3978, '::', () => { + console.log('Server Up'); +}); -#### Customer <-> Bot +// Replace this functions with custom login/verification for agents +const isAgent = (session) => session.message.user.name.startsWith("Agent"); -Customers connect to a bot as normal, through whatever channel the bot author allows. This conversation is in state *Bot*. +/** + bot: builder.UniversalBot + app: express ( e.g. const app = express(); ) + isAgent: function to determine when agent is talking to the bot + options: { } +**/ +handoff.setup(bot, app, isAgent, { + mongodbProvider: process.env.MONGODB_PROVIDER, + directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET, + textAnalyticsKey: process.env.CG_SENTIMENT_KEY, + appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY, + retainData: process.env.RETAIN_DATA, + customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND +}); -#### Customer Waiting +``` -A customer can enter a *Waiting* state. In this state, their messages are no longer sent to the bot. -If they send any messages, they will be informed that they are waiting to be connected to an agent. +### Settings + +You can either provide these settings in the options of `handoff.setup()` or just provide them as environment variables. + +#### mongodbProvider +`{mongodbProvider: process.env.MONGODB_PROVIDER}` + +mongodbProvider is a required field. This is your mongodb connection string. + +#### directlineSecret +`{directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET}` + +directlineSecret is a required field. This is your bot's direct line sectet key; you can get this from the bot framework portal when you setup the direct line channel. + +#### textAnalyticsKey +`{textAnalyticsKey: process.env.CG_SENTIMENT_KEY}` + +textAnalyticsKey is optional. This is the Microsoft Cognitive Services Text Analytics key. Providing this value will result in running sentiment analysis on all user messages, saving the sentiment score to the transcript in mongodb. + +#### appInsightsInstrumentationKey +`{appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY}` + +appInsightsInstrumentationKey is optional. This is the Microsoft Application Insights Instrumentation Key. Providing this value will result in logging most values of the conversation object to application insights as a custom event called 'Transcript'. + +The values logged to application ingsights are: + +``` javascript +botId +customerId +customerName +customerChannelId +customerConversationId + +//If the user has spoken to an agent, these values are also logged: + +agentId +agentName +agentChannelId +agentConversationId + +``` + +#### retainData +`{ retainData: process.env.RETAIN_DATA }` + +retainData is optional. If you want to keep the data after a hand off, you must add this environment variable/option. Otherwise, after an agent disconnects from talking to the user, the entire conversation object will be deleted from the database. This can be `"true"` or `"false"`. + +#### customerStartHandoffCommand +`{customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND}` + +customerStartHandoffCommand is optional. This is the command that a user (customer, not agent) can type to start the handoff which will queue them to speak to an agent. The default command will be set to `"help"`. Regex is used on this command to make sure the activation of the handoff only works if the user types the exact phrase provided in this property. + +#### Required environment variables: +``` +"MICROSOFT_APP_ID" : "", +"MICROSOFT_APP_PASSWORD" : "", +"MICROSOFT_DIRECTLINE_SECRET" : "", +"MONGODB_PROVIDER" : "" +``` + +#### Optional environment variables: +``` +"CG_SENTIMENT_KEY" : "", +"APPINSIGHTS_INSTRUMENTATIONKEY" : "", +"RETAIN_DATA: "true" or "false" +"CUSTOMER_START_HANDOFF_COMMAND" : "" +``` + +### Sample Webchat + +If you want the sample `/webchat` endpoint to work (endpoint for the example agent / call center), you will need to include this [`public` folder](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff/example/public) in the root directory of your project, or replace with your own. + +## Hand Off Details Depending on how your bot is written, this could happen via any or all of: diff --git a/app.ts b/app.ts deleted file mode 100644 index 7940ce4..0000000 --- a/app.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as express from 'express'; -import * as builder from 'botbuilder'; -import { Handoff } from './handoff'; -import { commandsMiddleware } from './commands'; - -//========================================================= -// Bot Setup -//========================================================= - -const app = express(); - -// Setup Express Server -app.listen(process.env.port || process.env.PORT || 3978, '::', () => { - console.log('Server Up'); -}); -// Create chat bot -const connector = new builder.ChatConnector({ - appId: process.env.MICROSOFT_APP_ID, - appPassword: process.env.MICROSOFT_APP_PASSWORD -}); - -const bot = new builder.UniversalBot(connector, [ - function (session, args, next) { - session.send('Echo ' + session.message.text); - } -]); - -app.post('/api/messages', connector.listen()); - -// Create endpoint for agent / call center -app.use('/webchat', express.static('public')); - -// replace this function with custom login/verification for agents -const isAgent = (session: builder.Session) => - session.message.user.name.startsWith("Agent"); - -const handoff = new Handoff(bot, isAgent); - -//======================================================== -// Bot Middleware -//======================================================== -bot.use( - commandsMiddleware(handoff), - handoff.routingMiddleware(), - /* other bot middlware should probably go here */ -); - diff --git a/commands.ts b/commands.ts deleted file mode 100644 index cfe6720..0000000 --- a/commands.ts +++ /dev/null @@ -1,167 +0,0 @@ -import * as builder from 'botbuilder'; -import { Conversation, ConversationState, Handoff } from './handoff'; - -export function commandsMiddleware(handoff: Handoff) { - return { - botbuilder: (session: builder.Session, next: Function) => { - if (session.message.type === 'message') { - command(session, next, handoff); - } - } - } -} - -function command( - session: builder.Session, - next: Function, - handoff: Handoff -) { - if (handoff.isAgent(session)) { - agentCommand(session, next, handoff); - } else { - customerCommand(session, next, handoff); - } -} - -function agentCommand( - session: builder.Session, - next: Function, - handoff: Handoff -) { - const message = session.message; - const conversation = handoff.getConversation({ agentConversationId: message.address.conversation.id }); - const inputWords = message.text.split(' '); - - if (inputWords.length == 0) - return; - - if (message.text === 'disconnect') { - disconnectCustomer(conversation, handoff, session); - return; - } - - // Commands to execute whether connected to a customer or not - switch (inputWords[0]) { - case 'options': - sendAgentCommandOptions(session); - return; - case 'list': - session.send(currentConversations(handoff)); - return; - case 'history': - handoff.getCustomerTranscript( - inputWords.length > 1 - ? { customerName: inputWords.slice(1).join(' ') } - : { agentConversationId: message.address.conversation.id }, - session); - return; - case 'waiting': - if (conversation) { - //disconnect from current conversation if already watching/talking - disconnectCustomer(conversation, handoff, session); - } - const waitingConversation = handoff.connectCustomerToAgent( - { bestChoice: true }, - ConversationState.Agent, - message.address - ); - if (waitingConversation) { - session.send("You are connected to " + waitingConversation.customer.user.name); - } else { - session.send("No customers waiting."); - } - return; - case 'connect': - case 'watch': - let newConversation; - if (inputWords[0] === 'connect') { - newConversation = handoff.connectCustomerToAgent( - inputWords.length > 1 - ? { customerName: inputWords.slice(1).join(' ') } - : { customerConversationId: conversation.customer.conversation.id }, - ConversationState.Agent, - message.address - ); - } else { - // watch currently only supports specifying a customer to watch - newConversation = handoff.connectCustomerToAgent( - { customerName: inputWords.slice(1).join(' ') }, - ConversationState.Watch, - message.address - ); - } - - if (newConversation) { - session.send("You are connected to " + newConversation.customer.user.name); - return; - } else { - session.send("something went wrong."); - } - return; - default: - if (conversation && conversation.state === ConversationState.Agent) { - return next(); - } - sendAgentCommandOptions(session); - return; - } -} - -function customerCommand(session: builder.Session, next: Function, handoff: Handoff) { - const message = session.message; - if (message.text === 'help') { - // lookup the conversation (create it if one doesn't already exist) - const conversation = handoff.getConversation({ customerConversationId: message.address.conversation.id }, message.address); - - if (conversation.state == ConversationState.Bot) { - handoff.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message.text); - handoff.queueCustomerForAgent({ customerConversationId: conversation.customer.conversation.id }) - session.send("Connecting you to the next available agent."); - return; - } - } - - return next(); -} - - -function sendAgentCommandOptions(session: builder.Session) { - const commands = ' ### Agent Options\n - Type *waiting* to connect to customer who has been waiting longest.\n - Type *connect { user name }* to connect to a specific conversation\n - Type *watch { user name }* to monitor a customer conversation\n - Type *history { user name }* to see a transcript of a given user\n - Type *list* to see a list of all current conversations.\n - Type *disconnect* while talking to a user to end a conversation.\n - Type *options* at any time to see these options again.'; - session.send(commands); - return; -} - -function currentConversations(handoff) { - const conversations = handoff.currentConversations(); - if (conversations.length === 0) { - return "No customers are in conversation."; - } - - let text = '### Current Conversations \n'; - conversations.forEach(conversation => { - const starterText = ' - *' + conversation.customer.user.name + '*'; - switch (ConversationState[conversation.state]) { - case 'Bot': - text += starterText + ' is talking to the bot\n'; - break; - case 'Agent': - text += starterText + ' is talking to an agent\n'; - break; - case 'Waiting': - text += starterText + ' is waiting to talk to an agent\n'; - break; - case 'Watch': - text += starterText + ' is being monitored by an agent\n'; - break; - } - }); - - return text; -} - -function disconnectCustomer(conversation: Conversation, handoff: any, session: builder.Session) { - if (handoff.connectCustomerToBot({ customerConversationId: conversation.customer.conversation.id })) { - session.send("Customer " + conversation.customer.user.name + " is now connected to the bot."); - } - -} diff --git a/example/app.ts b/example/app.ts new file mode 100644 index 0000000..ad2ef2f --- /dev/null +++ b/example/app.ts @@ -0,0 +1,50 @@ +import * as express from 'express'; +import * as builder from 'botbuilder'; +import * as handoff from 'botbuilder-handoff'; + +//========================================================= +// Normal Bot Setup +//========================================================= + +const app = express(); + +// Setup Express Server +app.listen(process.env.port || process.env.PORT || 3978, '::', () => { + console.log('Server Up'); +}); + +// Create chat bot +const connector = new builder.ChatConnector({ + appId: process.env.MICROSOFT_APP_ID, + appPassword: process.env.MICROSOFT_APP_PASSWORD +}); + +app.post('/api/messages', connector.listen()); + +const bot = new builder.UniversalBot(connector, [ + function (session, args, next) { + session.endConversation('Echo ' + session.message.text); + } +]); + +//========================================================= +// Hand Off Setup +//========================================================= + +// Replace this function with custom login/verification for agents +const isAgent = (session: builder.Session) => session.message.user.name.startsWith("Agent"); + +/** + bot: builder.UniversalBot + app: express ( e.g. const app = express(); ) + isAgent: function to determine when agent is talking to the bot + options: { } +**/ +handoff.setup(bot, app, isAgent, { + mongodbProvider: process.env.MONGODB_PROVIDER, + directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET, + textAnalyticsKey: process.env.CG_SENTIMENT_KEY, + appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY, + retainData: process.env.RETAIN_DATA, + customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND +}); \ No newline at end of file diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..903be6a --- /dev/null +++ b/example/package.json @@ -0,0 +1,27 @@ +{ + "name": "handoffbot", + "version": "0.1.0", + "license": "MIT", + "description": "Create example to escalate to a human", + "main": "app.js", + "scripts": { + "postinstall": "tsc", + "build": "tsc", + "watch": "tsc -w", + "nodewatch": "nodemon dist/app.js", + "start": "node dist/app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "botbuilder-handoff": "^0.1.7", + "botbuilder": "^3.7.0", + "express": "^4.14.0", + "typescript": "^2.1.4" + }, + "devDependencies": { + "@types/express": "^4.0.34", + "@types/mongoose": "^4.7.11", + "@types/node": "^6.0.59", + "nodemon": "^1.11.0" + } +} diff --git a/public/connectToBot.js b/example/public/connectToBot.js similarity index 100% rename from public/connectToBot.js rename to example/public/connectToBot.js diff --git a/public/index.html b/example/public/index.html similarity index 100% rename from public/index.html rename to example/public/index.html diff --git a/example/tsconfig.json b/example/tsconfig.json new file mode 100755 index 0000000..05ca984 --- /dev/null +++ b/example/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": false, + "outDir": "./dist", + "sourceMap": true + }, + "files": [ + "app.ts" + ] +} diff --git a/handoff-publish/LICENSE b/handoff-publish/LICENSE new file mode 100644 index 0000000..03fb838 --- /dev/null +++ b/handoff-publish/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/handoff-publish/README.md b/handoff-publish/README.md new file mode 100644 index 0000000..8c4a4e3 --- /dev/null +++ b/handoff-publish/README.md @@ -0,0 +1,125 @@ +# botbuilder-handoff + +A common request from companies and organizations considering bots is the ability to "hand off" a customer from a bot to a human agent, as seamlessly as possible. + +This project implements a framework called **Handoff** which enables bot authors to implement a wide variety of scenarios, including a full-fledged call center app, with minimal changes to the actual bot. + +It also includes a very simple implementation that illustrates the core concepts with minimal configuration. + +This project is in heavy flux, but is now in a usable state. However this should still be considered a sample, and not an officially supported Microsoft product. + +This project is written in TypeScript. + +[Source Code](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff) + +See [example folder](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff/example) for a full bot example. + +## Basic Usage + +```javascript +// Imports +const express = require('express'); +const builder = require('botbuilder'); +const handoff = require('botbuilder-handoff'); + +// Setup Express Server (N.B: If you are already using restify for your bot, you will need replace it with an express server) +const app = express(); +app.listen(process.env.port || process.env.PORT || 3978, '::', () => { + console.log('Server Up'); +}); + +// Replace this functions with custom login/verification for agents +const isAgent = (session) => session.message.user.name.startsWith("Agent"); + +/** + bot: builder.UniversalBot + app: express ( e.g. const app = express(); ) + isAgent: function to determine when agent is talking to the bot + options: { } +**/ +handoff.setup(bot, app, isAgent, { + mongodbProvider: process.env.MONGODB_PROVIDER, + directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET, + textAnalyticsKey: process.env.CG_SENTIMENT_KEY, + appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY, + retainData: process.env.RETAIN_DATA, + customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND +}); + +``` + +### Settings + +You can either provide these settings in the options of `handoff.setup()` or just provide them as environment variables. + +#### mongodbProvider +`{mongodbProvider: process.env.MONGODB_PROVIDER}` + +mongodbProvider is a required field. This is your mongodb connection string. + +#### directlineSecret +`{directlineSecret: process.env.MICROSOFT_DIRECTLINE_SECRET}` + +directlineSecret is a required field. This is your bot's direct line sectet key; you can get this from the bot framework portal when you setup the direct line channel. + +#### textAnalyticsKey +`{textAnalyticsKey: process.env.CG_SENTIMENT_KEY}` + +textAnalyticsKey is optional. This is the Microsoft Cognitive Services Text Analytics key. Providing this value will result in running sentiment analysis on all user messages, saving the sentiment score to the transcript in mongodb. + +#### appInsightsInstrumentationKey +`{appInsightsInstrumentationKey: process.env.APPINSIGHTS_INSTRUMENTATIONKEY}` + +appInsightsInstrumentationKey is optional. This is the Microsoft Application Insights Instrumentation Key. Providing this value will result in logging most values of the conversation object to application insights as a custom event called 'Transcript'. + +The values logged to application ingsights are: + +``` javascript +botId +customerId +customerName +customerChannelId +customerConversationId + +//If the user has spoken to an agent, these values are also logged: + +agentId +agentName +agentChannelId +agentConversationId + +``` + +#### retainData +`{ retainData: process.env.RETAIN_DATA }` + +retainData is optional. If you want to keep the data after a hand off, you must add this environment variable/option. Otherwise, after an agent disconnects from talking to the user, the entire conversation object will be deleted from the database. This can be `"true"` or `"false"`. + +#### customerStartHandoffCommand +`{customerStartHandoffCommand: process.env.CUSTOMER_START_HANDOFF_COMMAND}` + +customerStartHandoffCommand is optional. This is the command that a user (customer, not agent) can type to start the handoff which will queue them to speak to an agent. The default command will be set to `"help"`. Regex is used on this command to make sure the activation of the handoff only works if the user types the exact phrase provided in this property. + +#### Required environment variables: +``` +"MICROSOFT_APP_ID" : "", +"MICROSOFT_APP_PASSWORD" : "", +"MICROSOFT_DIRECTLINE_SECRET" : "", +"MONGODB_PROVIDER" : "" +``` + +#### Optional environment variables: +``` +"CG_SENTIMENT_KEY" : "", +"APPINSIGHTS_INSTRUMENTATIONKEY" : "", +"RETAIN_DATA: "true" or "false" +"CUSTOMER_START_HANDOFF_COMMAND" : "" +``` + +### Sample Webchat + +If you want the sample `/webchat` endpoint to work (endpoint for the example agent / call center), you will need to include this [`public` folder](https://github.com/palindromed/Bot-HandOff/tree/npm-handoff/example/public) in the root directory of your project, or replace with your own. + +## License + +MIT License \ No newline at end of file diff --git a/handoff-publish/commands.js b/handoff-publish/commands.js new file mode 100644 index 0000000..fb470ce --- /dev/null +++ b/handoff-publish/commands.js @@ -0,0 +1,142 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const handoff_1 = require("./handoff"); +const indexExports = require('./index'); +function commandsMiddleware(handoff) { + return { + botbuilder: (session, next) => { + if (session.message.type === 'message') { + command(session, next, handoff); + } + else { + // allow messages of non 'message' type through + next(); + } + } + }; +} +exports.commandsMiddleware = commandsMiddleware; +function command(session, next, handoff) { + if (handoff.isAgent(session)) { + agentCommand(session, next, handoff); + } + else { + customerCommand(session, next, handoff); + } +} +function agentCommand(session, next, handoff) { + return __awaiter(this, void 0, void 0, function* () { + const message = session.message; + const conversation = yield handoff.getConversation({ agentConversationId: message.address.conversation.id }); + const inputWords = message.text.split(' '); + if (inputWords.length == 0) + return; + // Commands to execute whether connected to a customer or not + if (inputWords[0] === 'options') { + sendAgentCommandOptions(session); + return; + } + else if (inputWords[0] === 'list') { + session.send(yield currentConversations(handoff)); + return; + } + // Commands to execute when not connected to a customer + if (!conversation) { + switch (inputWords[0]) { + case 'connect': + const newConversation = yield handoff.connectCustomerToAgent(inputWords.length > 1 + ? { customerName: inputWords.slice(1).join(' ') } + : { bestChoice: true }, message.address); + if (newConversation) { + session.send("You are connected to " + newConversation.customer.user.name); + } + else { + session.send("No customers waiting."); + } + break; + default: + sendAgentCommandOptions(session); + break; + } + return; + } + if (conversation.state !== handoff_1.ConversationState.Agent) { + // error state -- should not happen + session.send("Shouldn't be in this state - agent should have been cleared out."); + console.log("Shouldn't be in this state - agent should have been cleared out"); + return; + } + if (message.text === 'disconnect') { + if (yield handoff.connectCustomerToBot({ customerConversationId: conversation.customer.conversation.id })) { + session.send("Customer " + conversation.customer.user.name + " is now connected to the bot."); + } + return; + } + next(); + }); +} +function customerCommand(session, next, handoff) { + return __awaiter(this, void 0, void 0, function* () { + const message = session.message; + const customerStartHandoffCommandRegex = new RegExp("^" + indexExports._customerStartHandoffCommand + "$", "gi"); + if (customerStartHandoffCommandRegex.test(message.text)) { + // lookup the conversation (create it if one doesn't already exist) + const conversation = yield handoff.getConversation({ customerConversationId: message.address.conversation.id }, message.address); + if (conversation.state == handoff_1.ConversationState.Bot) { + yield handoff.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message); + yield handoff.queueCustomerForAgent({ customerConversationId: conversation.customer.conversation.id }); + session.endConversation("Connecting you to the next available agent."); + return; + } + } + return next(); + }); +} +function sendAgentCommandOptions(session) { + const commands = ' ### Agent Options\n - Type *waiting* to connect to customer who has been waiting longest.\n - Type *connect { user name }* to connect to a specific conversation\n - Type *watch { user name }* to monitor a customer conversation\n - Type *history { user name }* to see a transcript of a given user\n - Type *list* to see a list of all current conversations.\n - Type *disconnect* while talking to a user to end a conversation.\n - Type *options* at any time to see these options again.'; + session.send(commands); + return; +} +function currentConversations(handoff) { + return __awaiter(this, void 0, void 0, function* () { + const conversations = yield handoff.getCurrentConversations(); + if (conversations.length === 0) { + return "No customers are in conversation."; + } + let text = '### Current Conversations \n'; + conversations.forEach(conversation => { + const starterText = ' - *' + conversation.customer.user.name + '*'; + switch (handoff_1.ConversationState[conversation.state]) { + case 'Bot': + text += starterText + ' is talking to the bot\n'; + break; + case 'Agent': + text += starterText + ' is talking to an agent\n'; + break; + case 'Waiting': + text += starterText + ' is waiting to talk to an agent\n'; + break; + case 'Watch': + text += starterText + ' is being monitored by an agent\n'; + break; + } + }); + return text; + }); +} +function disconnectCustomer(conversation, handoff, session) { + return __awaiter(this, void 0, void 0, function* () { + if (yield handoff.connectCustomerToBot({ customerConversationId: conversation.customer.conversation.id })) { + session.send("Customer " + conversation.customer.user.name + " is now connected to the bot."); + } + }); +} +//# sourceMappingURL=commands.js.map \ No newline at end of file diff --git a/handoff-publish/handoff.js b/handoff-publish/handoff.js new file mode 100644 index 0000000..a968b13 --- /dev/null +++ b/handoff-publish/handoff.js @@ -0,0 +1,134 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const builder = require("botbuilder"); +const mongoose_provider_1 = require("./mongoose-provider"); +// Options for state of a conversation +// Customer talking to bot, waiting for next available agent or talking to an agent +var ConversationState; +(function (ConversationState) { + ConversationState[ConversationState["Bot"] = 0] = "Bot"; + ConversationState[ConversationState["Waiting"] = 1] = "Waiting"; + ConversationState[ConversationState["Agent"] = 2] = "Agent"; + ConversationState[ConversationState["Watch"] = 3] = "Watch"; +})(ConversationState = exports.ConversationState || (exports.ConversationState = {})); +; +class Handoff { + // if customizing, pass in your own check for isAgent and your own versions of methods in defaultProvider + constructor(bot, isAgent, provider = new mongoose_provider_1.MongooseProvider()) { + this.bot = bot; + this.isAgent = isAgent; + this.provider = provider; + this.connectCustomerToAgent = (by, agentAddress) => __awaiter(this, void 0, void 0, function* () { + return yield this.provider.connectCustomerToAgent(by, agentAddress); + }); + this.connectCustomerToBot = (by) => __awaiter(this, void 0, void 0, function* () { + return yield this.provider.connectCustomerToBot(by); + }); + this.queueCustomerForAgent = (by) => __awaiter(this, void 0, void 0, function* () { + return yield this.provider.queueCustomerForAgent(by); + }); + this.addToTranscript = (by, message) => __awaiter(this, void 0, void 0, function* () { + let from = by.agentConversationId ? 'Agent' : 'Customer'; + return yield this.provider.addToTranscript(by, message, from); + }); + this.getConversation = (by, customerAddress) => __awaiter(this, void 0, void 0, function* () { + return yield this.provider.getConversation(by, customerAddress); + }); + this.getCurrentConversations = () => __awaiter(this, void 0, void 0, function* () { + return yield this.provider.getCurrentConversations(); + }); + this.provider.init(); + } + routingMiddleware() { + return { + botbuilder: (session, next) => { + // Pass incoming messages to routing method + if (session.message.type === 'message') { + this.routeMessage(session, next); + } + else { + // allow messages of non 'message' type through + next(); + } + }, + send: (event, next) => __awaiter(this, void 0, void 0, function* () { + // Messages sent from the bot do not need to be routed + // Not all messages from the bot are type message, we only want to record the actual messages + if (event.type === 'message' && !event.entities) { + this.transcribeMessageFromBot(event, next); + } + else { + //If not a message (text), just send to user without transcribing + next(); + } + }) + }; + } + routeMessage(session, next) { + if (this.isAgent(session)) { + this.routeAgentMessage(session); + } + else { + this.routeCustomerMessage(session, next); + } + } + routeAgentMessage(session) { + return __awaiter(this, void 0, void 0, function* () { + const message = session.message; + const conversation = yield this.getConversation({ agentConversationId: message.address.conversation.id }, message.address); + yield this.addToTranscript({ agentConversationId: message.address.conversation.id }, message); + // if the agent is not in conversation, no further routing is necessary + if (!conversation) + return; + if (conversation.state !== ConversationState.Agent) { + // error state -- should not happen + session.send("Shouldn't be in this state - agent should have been cleared out."); + return; + } + // send text that agent typed to the customer they are in conversation with + this.bot.send(new builder.Message().address(conversation.customer).text(message.text).addEntity({ "agent": true })); + }); + } + routeCustomerMessage(session, next) { + return __awaiter(this, void 0, void 0, function* () { + const message = session.message; + // method will either return existing conversation or a newly created conversation if this is first time we've heard from customer + const conversation = yield this.getConversation({ customerConversationId: message.address.conversation.id }, message.address); + yield this.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message); + switch (conversation.state) { + case ConversationState.Bot: + return next(); + case ConversationState.Waiting: + session.send("Connecting you to the next available agent."); + return; + case ConversationState.Watch: + this.bot.send(new builder.Message().address(conversation.agent).text(message.text)); + return next(); + case ConversationState.Agent: + if (!conversation.agent) { + session.send("No agent address present while customer in state Agent"); + console.log("No agent address present while customer in state Agent"); + return; + } + this.bot.send(new builder.Message().address(conversation.agent).text(message.text)); + return; + } + }); + } + // These methods are wrappers around provider which handles data + transcribeMessageFromBot(message, next) { + this.provider.addToTranscript({ customerConversationId: message.address.conversation.id }, message, 'Bot'); + next(); + } +} +exports.Handoff = Handoff; +; +//# sourceMappingURL=handoff.js.map \ No newline at end of file diff --git a/handoff-publish/index.js b/handoff-publish/index.js new file mode 100644 index 0000000..481a152 --- /dev/null +++ b/handoff-publish/index.js @@ -0,0 +1,119 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const mongoose_provider_1 = require("./mongoose-provider"); +const handoff_1 = require("./handoff"); +const commands_1 = require("./commands"); +const express = require("express"); +const bodyParser = require("body-parser"); +const cors = require("cors"); +let appInsights = require('applicationinsights'); +let setup = (bot, app, isAgent, options) => { + let mongooseProvider = null; + let _retainData = null; + let _directLineSecret = null; + let _mongodbProvider = null; + let _textAnalyticsKey = null; + let _appInsightsInstrumentationKey = null; + let _customerStartHandoffCommand = null; + const handoff = new handoff_1.Handoff(bot, isAgent); + options = options || {}; + if (!options.mongodbProvider && !process.env.MONGODB_PROVIDER) { + throw new Error('Bot-Handoff: Mongo DB Connection String was not provided in setup options (mongodbProvider) or in the environment variables (MONGODB_PROVIDER)'); + } + else { + _mongodbProvider = options.mongodbProvider || process.env.MONGODB_PROVIDER; + mongooseProvider = new mongoose_provider_1.MongooseProvider(); + mongoose_provider_1.mongoose.connect(_mongodbProvider); + } + if (!options.directlineSecret && !process.env.MICROSOFT_DIRECTLINE_SECRET) { + throw new Error('Bot-Handoff: Microsoft Bot Builder Direct Line Secret was not provided in setup options (directlineSecret) or in the environment variables (MICROSOFT_DIRECTLINE_SECRET)'); + } + else { + _directLineSecret = options.directlineSecret || process.env.MICROSOFT_DIRECTLINE_SECRET; + } + if (!options.textAnalyticsKey && !process.env.CG_SENTIMENT_KEY) { + console.warn('Bot-Handoff: Microsoft Cognitive Services Text Analytics Key was not provided in setup options (textAnalyticsKey) or in the environment variables (CG_SENTIMENT_KEY). Sentiment will not be analysed in the transcript, the score will be recorded as -1 for all text.'); + } + else { + _textAnalyticsKey = options.textAnalyticsKey || process.env.CG_SENTIMENT_KEY; + exports._textAnalyticsKey = _textAnalyticsKey; + } + if (!options.appInsightsInstrumentationKey && !process.env.APPINSIGHTS_INSTRUMENTATIONKEY) { + console.warn('Bot-Handoff: Microsoft Application Insights Instrumentation Key was not provided in setup options (appInsightsInstrumentationKey) or in the environment variables (APPINSIGHTS_INSTRUMENTATIONKEY). The conversation object will not be logged to Application Insights.'); + } + else { + _appInsightsInstrumentationKey = options.appInsightsInstrumentationKey || process.env.APPINSIGHTS_INSTRUMENTATIONKEY; + appInsights.setup(_appInsightsInstrumentationKey).start(); + exports._appInsights = appInsights; + } + if (!options.retainData && !process.env.RETAIN_DATA) { + console.warn('Bot-Handoff: Retain data value was not provided in setup options (retainData) or in the environment variables (RETAIN_DATA). Not providing this value or setting it to "false" means that if a customer speaks to an agent, the conversation record with that customer will be deleted after an agent disconnects the conversation. Set to "true" to keep all data records in the mongo database.'); + } + else { + _retainData = options.retainData || process.env.RETAIN_DATA; + exports._retainData = _retainData; + } + if (!options.customerStartHandoffCommand && !process.env.CUSTOMER_START_HANDOFF_COMMAND) { + console.warn('Bot-Handoff: The customer command to start the handoff was not provided in setup options (customerStartHandoffCommand) or in the environment variables (CUSTOMER_START_HANDOFF_COMMAND). The default command will be set to help. Regex is used on this command to make sure the activation of the handoff only works if the user types the exact phrase provided in this property.'); + _customerStartHandoffCommand = "help"; + exports._customerStartHandoffCommand = _customerStartHandoffCommand; + } + else { + _customerStartHandoffCommand = options.customerStartHandoffCommand || process.env.CUSTOMER_START_HANDOFF_COMMAND; + exports._customerStartHandoffCommand = _customerStartHandoffCommand; + } + if (bot) { + bot.use(commands_1.commandsMiddleware(handoff), handoff.routingMiddleware()); + } + if (app && _directLineSecret != null) { + app.use(cors({ origin: '*' })); + app.use(bodyParser.json()); + // Create endpoint for agent / call center + app.use('/webchat', express.static('public')); + // Endpoint to get current conversations + app.get('/api/conversations', (req, res) => __awaiter(this, void 0, void 0, function* () { + const authHeader = req.headers['authorization']; + console.log(authHeader); + console.log(req.headers); + if (authHeader) { + if (authHeader === 'Bearer ' + _directLineSecret) { + let conversations = yield mongooseProvider.getCurrentConversations(); + res.status(200).send(conversations); + } + } + res.status(401).send('Not Authorized'); + })); + // Endpoint to trigger handover + app.post('/api/conversations', (req, res) => __awaiter(this, void 0, void 0, function* () { + const authHeader = req.headers['authorization']; + console.log(authHeader); + console.log(req.headers); + if (authHeader) { + if (authHeader === 'Bearer ' + _directLineSecret) { + if (yield handoff.queueCustomerForAgent({ customerConversationId: req.body.conversationId })) { + res.status(200).send({ "code": 200, "message": "OK" }); + } + else { + res.status(400).send({ "code": 400, "message": "Can't find conversation ID" }); + } + } + } + else { + res.status(401).send({ "code": 401, "message": "Not Authorized" }); + } + })); + } + else { + throw new Error('Microsoft Bot Builder Direct Line Secret was not provided in options or the environment variable MICROSOFT_DIRECTLINE_SECRET'); + } +}; +module.exports = { setup }; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/handoff-publish/mongoose-provider.js b/handoff-publish/mongoose-provider.js new file mode 100644 index 0000000..d51490d --- /dev/null +++ b/handoff-publish/mongoose-provider.js @@ -0,0 +1,272 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const bluebird = require("bluebird"); +const request = require("request"); +const _ = require("lodash"); +const mongoose = require("mongoose"); +exports.mongoose = mongoose; +mongoose.Promise = bluebird; +const handoff_1 = require("./handoff"); +const indexExports = require('./index'); +// ------------------- +// Bot Framework types +// ------------------- +exports.IIdentitySchema = new mongoose.Schema({ + id: { type: String, required: true }, + isGroup: { type: Boolean, required: false }, + name: { type: String, required: false }, +}, { + _id: false, + strict: false, +}); +exports.IAddressSchema = new mongoose.Schema({ + bot: { type: exports.IIdentitySchema, required: true }, + channelId: { type: String, required: true }, + conversation: { type: exports.IIdentitySchema, required: false }, + user: { type: exports.IIdentitySchema, required: true }, + id: { type: String, required: false }, + serviceUrl: { type: String, required: false }, + useAuth: { type: Boolean, required: false } +}, { + strict: false, + id: false, + _id: false +}); +// ------------- +// Handoff types +// ------------- +exports.TranscriptLineSchema = new mongoose.Schema({ + timestamp: {}, + from: String, + sentimentScore: Number, + state: Number, + text: String +}); +exports.ConversationSchema = new mongoose.Schema({ + customer: { type: exports.IAddressSchema, required: true }, + agent: { type: exports.IAddressSchema, required: false }, + state: { + type: Number, + required: true, + min: 0, + max: 3 + }, + transcript: [exports.TranscriptLineSchema] +}); +exports.ConversationModel = mongoose.model('Conversation', exports.ConversationSchema); +exports.BySchema = new mongoose.Schema({ + bestChoice: Boolean, + agentConversationId: String, + customerConversationId: String, + customerName: String +}); +exports.ByModel = mongoose.model('By', exports.BySchema); +// ----------------- +// Mongoose Provider +// ----------------- +class MongooseProvider { + init() { } + addToTranscript(by, message, from) { + return __awaiter(this, void 0, void 0, function* () { + let sentimentScore = -1; + let text = message.text; + let datetime = new Date().toISOString(); + const conversation = yield this.getConversation(by); + if (!conversation) + return false; + if (from == "Customer") { + if (indexExports._textAnalyticsKey) { + sentimentScore = yield this.collectSentiment(text); + } + datetime = message.localTimestamp ? message.localTimestamp : message.timestamp; + } + conversation.transcript.push({ + timestamp: datetime, + from: from, + sentimentScore: sentimentScore, + state: conversation.state, + text + }); + if (indexExports._appInsights) { + // You can't log embedded json objects in application insights, so we are flattening the object to one item. + // Also, have to stringify the object so functions from mongodb don't get logged + let latestTranscriptItem = conversation.transcript.length - 1; + let x = JSON.parse(JSON.stringify(conversation.transcript[latestTranscriptItem])); + x['botId'] = conversation.customer.bot.id; + x['customerId'] = conversation.customer.user.id; + x['customerName'] = conversation.customer.user.name; + x['customerChannelId'] = conversation.customer.channelId; + x['customerConversationId'] = conversation.customer.conversation.id; + if (conversation.agent) { + x['agentId'] = conversation.agent.user.id; + x['agentName'] = conversation.agent.user.name; + x['agentChannelId'] = conversation.agent.channelId; + x['agentConversationId'] = conversation.agent.conversation.id; + } + indexExports._appInsights.client.trackEvent("Transcript", x); + } + return yield this.updateConversation(conversation); + }); + } + connectCustomerToAgent(by, agentAddress) { + return __awaiter(this, void 0, void 0, function* () { + const conversation = yield this.getConversation(by); + if (conversation) { + conversation.state = handoff_1.ConversationState.Agent; + conversation.agent = agentAddress; + } + const success = yield this.updateConversation(conversation); + if (success) + return conversation; + else + return null; + }); + } + queueCustomerForAgent(by) { + return __awaiter(this, void 0, void 0, function* () { + const conversation = yield this.getConversation(by); + if (!conversation) { + return false; + } + else { + conversation.state = handoff_1.ConversationState.Waiting; + return yield this.updateConversation(conversation); + } + }); + } + connectCustomerToBot(by) { + return __awaiter(this, void 0, void 0, function* () { + const conversation = yield this.getConversation(by); + if (!conversation) { + return false; + } + else { + conversation.state = handoff_1.ConversationState.Bot; + if (indexExports._retainData === "true") { + return yield this.updateConversation(conversation); + } + else { + if (conversation.agent) { + return yield this.deleteConversation(conversation); + } + else { + return yield this.updateConversation(conversation); + } + } + } + }); + } + getConversation(by, customerAddress) { + return __awaiter(this, void 0, void 0, function* () { + if (by.customerName) { + return yield exports.ConversationModel.findOne({ 'customer.user.name': by.customerName }); + } + else if (by.agentConversationId) { + const conversation = yield exports.ConversationModel.findOne({ 'agent.conversation.id': by.agentConversationId }); + if (conversation) + return conversation; + else + return null; + } + else if (by.customerConversationId) { + let conversation = yield exports.ConversationModel.findOne({ 'customer.conversation.id': by.customerConversationId }); + if (!conversation && customerAddress) { + conversation = yield this.createConversation(customerAddress); + } + return conversation; + } + return null; + }); + } + getCurrentConversations() { + return __awaiter(this, void 0, void 0, function* () { + let conversations; + try { + conversations = yield exports.ConversationModel.find(); + } + catch (error) { + console.log('Failed loading conversations'); + console.log(error); + } + return conversations; + }); + } + createConversation(customerAddress) { + return __awaiter(this, void 0, void 0, function* () { + return yield exports.ConversationModel.create({ + customer: customerAddress, + state: handoff_1.ConversationState.Bot, + transcript: [] + }); + }); + } + updateConversation(conversation) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve, reject) => { + exports.ConversationModel.findByIdAndUpdate(conversation._id, conversation).then((error) => { + resolve(true); + }).catch((error) => { + console.log('Failed to update conversation'); + console.log(conversation); + resolve(false); + }); + }); + }); + } + deleteConversation(conversation) { + return __awaiter(this, void 0, void 0, function* () { + return new Promise((resolve) => { + exports.ConversationModel.findByIdAndRemove(conversation._id).then((error) => { + resolve(true); + }); + }); + }); + } + collectSentiment(text) { + return __awaiter(this, void 0, void 0, function* () { + if (text == null || text == '') + return; + let _sentimentUrl = 'https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment'; + let _sentimentId = 'bot-analytics'; + let _sentimentKey = indexExports._textAnalyticsKey; + let options = { + url: _sentimentUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': _sentimentKey + }, + json: true, + body: { + "documents": [ + { + "language": "en", + "id": _sentimentId, + "text": text + } + ] + } + }; + return new Promise(function (resolve, reject) { + request(options, (error, response, body) => { + if (error) { + reject(error); + } + let result = _.find(body.documents, { id: _sentimentId }) || {}; + let score = result.score || null; + resolve(score); + }); + }); + }); + } +} +exports.MongooseProvider = MongooseProvider; +//# sourceMappingURL=mongoose-provider.js.map \ No newline at end of file diff --git a/handoff-publish/package.json b/handoff-publish/package.json new file mode 100644 index 0000000..400a1e4 --- /dev/null +++ b/handoff-publish/package.json @@ -0,0 +1,31 @@ +{ + "name": "botbuilder-handoff", + "version": "0.1.8", + "license": "MIT", + "description": "Bot hand off module for the Microsoft Bot Framework. It allows you to transfer a customer from talking to a bot to talking to a human.", + "main": "index.js", + "homepage": "https://github.com/palindromed/Bot-HandOff/tree/npm-handoff", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/palindromed/Bot-HandOff.git" + }, + "keywords": [ + "bot", + "handover", + "hand off", + "bot framework" + ], + "dependencies": { + "applicationinsights": "^0.20.1", + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "botbuilder": "^3.7.0", + "cors": "2.8.3", + "express": "^4.14.0", + "mongoose": "^4.9.7" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..169a486 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3071 @@ +{ + "name": "botbuilder-handoff", + "version": "0.1.8", + "lockfileVersion": 1, + "dependencies": { + "@types/bson": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.3.tgz", + "integrity": "sha1-bCbwh2v52Muwbt1AGeKTVL86A+A=", + "dev": true + }, + "@types/chai": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.1.tgz", + "integrity": "sha512-DWrdkraJO+KvBB7+Jc6AuDd2+fwV6Z9iK8cqEEoYpcurYrH7GiUZmwjFuQIIWj5HhFz6NsSxdN72YMIHT7Fy2Q==", + "dev": true + }, + "@types/express": { + "version": "4.0.36", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.0.36.tgz", + "integrity": "sha512-bT9q2eqH/E72AGBQKT50dh6AXzheTqigGZ1GwDiwmx7vfHff0bZOrvUWjvGpNWPNkRmX1vDF6wonG6rlpBHb1A==", + "dev": true + }, + "@types/express-serve-static-core": { + "version": "4.0.48", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz", + "integrity": "sha512-+W+fHO/hUI6JX36H8FlgdMHU3Dk4a/Fn08fW5qdd7MjPP/wJlzq9fkCrgaH0gES8vohVeqwefHwPa4ylVKyYIg==", + "dev": true + }, + "@types/mime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.1.tgz", + "integrity": "sha512-rek8twk9C58gHYqIrUlJsx8NQMhlxqHzln9Z9ODqiNgv3/s+ZwIrfr+djqzsnVM12xe9hL98iJ20lj2RvCBv6A==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.41", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.41.tgz", + "integrity": "sha1-4nzwgXFT658nE7LT9saPHhw8pgg=", + "dev": true + }, + "@types/mongodb": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-2.2.6.tgz", + "integrity": "sha512-IYMgWzhfLLYvMUmLvo3aaxSKYg+udWZ4j5vQJ+/uecRNFt5MnO2eI8nvbwwFYcIEwc1QRdFQZEL6JzcnE+DU9A==", + "dev": true + }, + "@types/mongoose": { + "version": "4.7.18", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-4.7.18.tgz", + "integrity": "sha512-DNgUKoVygZtUieTZ4Fc5ZdgVQr/exkzF1MRYnciimFSsZCDjRkdpjZ1altm9e/IpInUjb37nRwhkTBeX6RDULg==", + "dev": true + }, + "@types/node": { + "version": "6.0.78", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.78.tgz", + "integrity": "sha512-+vD6E8ixntRzzZukoF3uP1iV+ZjVN3koTcaeK+BEoc/kSfGbLDIGC7RmCaUgVpUfN6cWvfczFRERCyKM9mkvXg==", + "dev": true + }, + "@types/serve-static": { + "version": "1.7.31", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.7.31.tgz", + "integrity": "sha1-FUVt6NmNa0z/Mb5savdJKuY/Uho=", + "dev": true + }, + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true + }, + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=" + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", + "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "dev": true + }, + "applicationinsights": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/applicationinsights/-/applicationinsights-0.20.1.tgz", + "integrity": "sha1-+5Z+BRkzDAWT7rWvtaB4g2wkML4=" + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true + }, + "arr-flatten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.3.tgz", + "integrity": "sha1-onTthawIhJtr14R8RYB0XcUa37E=", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asap": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.5.tgz", + "integrity": "sha1-UidltQw1EEkOUtfc/ghe+bqWlY8=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=", + "dev": true + }, + "base64url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", + "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "optional": true + }, + "binary-extensions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", + "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", + "dev": true + }, + "bl": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.1.tgz", + "integrity": "sha1-ysMo977kVzDUBLaSID/LWQ4XLV4=", + "dev": true + }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, + "body-parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", + "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=" + }, + "bot-tester": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/bot-tester/-/bot-tester-0.0.30.tgz", + "integrity": "sha512-OHUflw4nbxP2BvB7ci0ocL6bMe2APQt0YahiZuMgTsbjbYXYh2YGlDegzsla8MLq3kiob3tvUMqYBrEnfxD02g==", + "dev": true, + "dependencies": { + "@types/node": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.32.tgz", + "integrity": "sha512-7+0Ai8r8Xt6NNVM0Eo+XSqiZsBUYXg2yrCwyBhQzSfFHTGQWzFv/pk9106vPR8HWjKmGK+zzUj244POs4xfO2g==", + "dev": true + } + } + }, + "botbuilder": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-3.8.4.tgz", + "integrity": "sha1-/O3EK5J+zwUMJL5SDBKAgkQ3pwg=" + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "bson": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", + "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" + }, + "buffer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", + "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" + }, + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.0.2.tgz", + "integrity": "sha1-L3MnxN5vOF3XeHmZ4qsCaXoyuDs=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true + }, + "chrono-node": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/chrono-node/-/chrono-node-1.3.4.tgz", + "integrity": "sha1-/CqSCGNuCdb9exLZSuJECTfeJL0=" + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=" + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true + }, + "configstore": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", + "integrity": "sha1-w1eB0FAdJowlxUuLF/YkDopPsCE=", + "dev": true, + "dependencies": { + "uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", + "dev": true + } + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cors": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.3.tgz", + "integrity": "sha1-TPeOHSMymnSWsvwiJbd8pbteuAI=" + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "debug": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz", + "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=", + "dev": true, + "dependencies": { + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + } + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decompress": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", + "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", + "dev": true + }, + "decompress-tar": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.0.tgz", + "integrity": "sha1-HwkqtphEBVjHL8eOd9JG0+y0U7A=", + "dev": true + }, + "decompress-tarbz2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.0.tgz", + "integrity": "sha1-+6tY1d5z8/0hPKw68cGDNPUcuJE=", + "dev": true + }, + "decompress-targz": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.0.tgz", + "integrity": "sha1-R1ucQGvmIa6DYnSALZsl+ZE+rVk=", + "dev": true, + "dependencies": { + "file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", + "dev": true + } + } + }, + "decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true + }, + "deep-eql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", + "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", + "dev": true, + "dependencies": { + "type-detect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", + "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", + "dev": true + } + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "diagnostic-channel": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.1.0.tgz", + "integrity": "sha1-emrYrVBmusVE2go3m6F0ujYqV88=" + }, + "diagnostic-channel-publishers": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.1.2.tgz", + "integrity": "sha1-M7LDMzkv1+lzMTtO+t+MYAv+ph4=" + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "dom-walk": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", + "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexify": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", + "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "optional": true + }, + "ecdsa-sig-formatter": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.9.tgz", + "integrity": "sha1-S8kmJ07Dtau1AW5+HWCSGsJisqE=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "end-of-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", + "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=", + "dev": true, + "dependencies": { + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true + } + } + }, + "es6-promise": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", + "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true + }, + "express": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", + "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true + }, + "file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true + }, + "finalhandler": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", + "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=" + }, + "forwarded": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=" + }, + "fresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs-extra": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.1.2.tgz", + "integrity": "sha1-BGxwFjzvmq1GsOSn+kZ/si1x3jU=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "dev": true, + "optional": true, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz", + "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=", + "dev": true + }, + "buffer-shims": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", + "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "optional": true + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=", + "dev": true, + "optional": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", + "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", + "dev": true, + "optional": true + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true + }, + "npmlog": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", + "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", + "dev": true, + "optional": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "optional": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "optional": true, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "integrity": "sha1-z3jsb0ptHrQ9JkiMrJfwQudLf8g=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true + }, + "safe-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz", + "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true + }, + "sshpk": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz", + "integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=", + "dev": true, + "optional": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.1.tgz", + "integrity": "sha1-YuIA8DmVWmgQ2N8KM//A8BNmLZg=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true + }, + "tar-pack": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", + "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "dev": true, + "optional": true + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true, + "optional": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", + "dev": true + }, + "getos": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/getos/-/getos-2.8.4.tgz", + "integrity": "sha1-e4YD02GcKOOMsP56T2PDrLgNUWM=", + "dev": true, + "dependencies": { + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=", + "dev": true + } + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true + }, + "global": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", + "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", + "dev": true + }, + "got": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", + "integrity": "sha1-5dDtSvVfw+701WAHdp2YGSvLLso=", + "dev": true, + "dependencies": { + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=" + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=" + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + }, + "hooks-fixed": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hooks-fixed/-/hooks-fixed-2.0.0.tgz", + "integrity": "sha1-oB2JTVKsf2WZu7H2PfycQR33DLo=" + }, + "http-errors": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=" + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=" + }, + "iconv-lite": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "infinity-agent": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", + "integrity": "sha1-ReDi/3qesDCyfWK3SzdEt6esQhY=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "ipaddr.js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", + "integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=" + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true + }, + "is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true + }, + "is-npm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isemail": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-1.2.0.tgz", + "integrity": "sha1-vgPfjMPineTSxd9lASY/H6RZXpo=" + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "joi": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-6.10.1.tgz", + "integrity": "sha1-TVDDGAeRIgAP5fFq8f+OGRe3fgY=" + }, + "js-tokens": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz", + "integrity": "sha1-COnxMkhKLEWjCQfp3E1VZ7fxFNc=", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonfile": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + }, + "jsonwebtoken": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-7.4.1.tgz", + "integrity": "sha1-fKMk9SFfi+A5zTWmxFu4y3SkSPs=" + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "jwa": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz", + "integrity": "sha1-oFUs4CIHQs1S4VN3SjKQXDDnVuU=" + }, + "jws": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.4.tgz", + "integrity": "sha1-+ei5M46KhHJ31kRLFGT2GIDgUKI=" + }, + "kareem": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-1.4.1.tgz", + "integrity": "sha1-7XYgAET6BB7zK02oJh4lU/EXNTE=" + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true + }, + "latest-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", + "integrity": "sha1-cs/Ebj6NG+ZR4eu1Tqn26pbzdLs=", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true + }, + "lodash.defaults": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", + "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "make-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.0.0.tgz", + "integrity": "sha1-l6ARdR6R3YfPre9Ygy67BJNt6Xg=", + "dev": true + }, + "make-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.0.tgz", + "integrity": "sha1-Uq06M5zPEM5itAQLcI/nByRLi5Y=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "md5-file": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-3.1.1.tgz", + "integrity": "sha1-2zySwJu9pcLeiD+lSQ3XEf3burk=", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=" + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "mocha": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", + "integrity": "sha1-0O9NMyEm2/GNDWQMmzgt1IvpdZQ=", + "dev": true + }, + "moment": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz", + "integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8=" + }, + "mongodb": { + "version": "2.2.29", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.29.tgz", + "integrity": "sha512-MrQvIsN6zN80I4hdFo8w46w51cIqD2FJBGsUfApX9GmjXA1aCclEAJbOHaQWjCtabeWq57S3ECzqEKg/9bdBhA==", + "dev": true, + "dependencies": { + "mongodb-core": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.13.tgz", + "integrity": "sha512-mbcvqLLZwVcpTrsfBDY3hRNk2SDNJWOvKKxFJSc0pnUBhYojymBc/L0THfQsWwKJrkb2nIXSjfFll1mG/I5OqQ==", + "dev": true + } + } + }, + "mongodb-core": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.11.tgz", + "integrity": "sha1-HDh3bOsXSZepnCiGDu2QKNqbPho=" + }, + "mongodb-download": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/mongodb-download/-/mongodb-download-2.2.3.tgz", + "integrity": "sha1-Wrk5bGwV92U3Vu1kbjByLwrXKgA=", + "dev": true + }, + "mongodb-prebuilt": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/mongodb-prebuilt/-/mongodb-prebuilt-6.3.3.tgz", + "integrity": "sha1-8aF7NFhsZznGvG3aGWhk4EZKg2w=", + "dev": true + }, + "mongoose": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.11.0.tgz", + "integrity": "sha1-du3FGVlSD0arbzIhVoM736NaK8A=", + "dependencies": { + "async": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.1.4.tgz", + "integrity": "sha1-LSFgx3iAMuTdbL4lAvH5osj2zeQ=" + }, + "mongodb": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.27.tgz", + "integrity": "sha1-NBIgNNtm2YO89qta2yaiSnD+9uY=" + } + } + }, + "mpath": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.3.0.tgz", + "integrity": "sha1-elj3iem1/TyUUgY0FXlg8mvV70Q=" + }, + "mpromise": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mpromise/-/mpromise-0.5.5.tgz", + "integrity": "sha1-9bJCWddjrMIlewoMjG2Gb9UXMuY=" + }, + "mquery": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-2.3.1.tgz", + "integrity": "sha1-mrNnSXFIAP8LtTpoHOS8TV8HyHs=", + "dependencies": { + "bluebird": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz", + "integrity": "sha1-AkpVFylTCIV/FPkfEQb8O1VfRGs=" + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + }, + "sliced": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-0.0.5.tgz", + "integrity": "sha1-XtwETKTrb3gW1Qui/GPiXY/kcH8=" + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "muri": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/muri/-/muri-1.2.1.tgz", + "integrity": "sha1-7H6lzmympSPrGrNbrNpfqBbJqjw=" + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "dev": true, + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "nested-error-stacks": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", + "integrity": "sha1-GfYZWRUZ8JZ2mlupqG5u7sgjw88=", + "dev": true + }, + "nodemon": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.11.0.tgz", + "integrity": "sha1-ImxWK9KnsT09dRi0mtSCijYj0Gw=", + "dev": true + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true + }, + "os-shim": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", + "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true + }, + "package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", + "integrity": "sha1-yOysCUInzfdqMWh07QXifMk5oOA=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "process": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", + "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==" + }, + "proxy-addr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz", + "integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=" + }, + "ps-tree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ps-tree/-/ps-tree-1.1.0.tgz", + "integrity": "sha1-tCGyQUDWID8e08dplrRCewjowBQ=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true + } + } + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=" + }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "read-all-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", + "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", + "dev": true + }, + "readable-stream": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", + "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=" + }, + "readdirp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", + "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "dev": true + }, + "regex-cache": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "dev": true + }, + "regexp-clone": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", + "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" + }, + "registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", + "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=" + }, + "request-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.1.tgz", + "integrity": "sha1-fuxWyJMXqCLL/qmbA5zlQ8LhX2c=", + "dev": true + }, + "request-promise-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", + "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "dev": true + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==" + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true + }, + "rsa-pem-from-mod-exp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/rsa-pem-from-mod-exp/-/rsa-pem-from-mod-exp-0.8.4.tgz", + "integrity": "sha1-NipCxtMEBW1JOz8SvOq7LGV2ptQ=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "seek-bzip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", + "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", + "dev": true, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dev": true + } + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true + }, + "send": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.3.tgz", + "integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=", + "dependencies": { + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=" + } + } + }, + "serve-static": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz", + "integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=" + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "dev": true + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=" + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true + }, + "spawn-sync": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", + "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "dev": true + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + } + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==" + }, + "string-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", + "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", + "dev": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-dirs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.0.0.tgz", + "integrity": "sha1-YQzbKSggDaAAT0HcuQ/JXNkZoLY=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true + }, + "tar-stream": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.5.4.tgz", + "integrity": "sha1-NlSc8E7RrumyowwBQyUiONr5QBY=", + "dev": true + }, + "test-with-mongo": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/test-with-mongo/-/test-with-mongo-0.0.8.tgz", + "integrity": "sha512-HztvDENbvx9KOGOzvhhhQXaOREszl2AjYve8DdfcurI/0SHqhCC7i1qZQD7Qr6kd1JudAxHTrygXGoToOPJUfg==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "timed-out": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", + "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=", + "dev": true + }, + "topo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-1.1.0.tgz", + "integrity": "sha1-6ddRYV0buH3IZdsYL6HKCl71NtU=" + }, + "touch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz", + "integrity": "sha1-RJy+LbrlqMgDjjDXH6D/RklHxN4=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=" + }, + "ts-node": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.1.0.tgz", + "integrity": "sha1-p17FrrSPMFixuUXbp2XxFQuoj4w=", + "dev": true, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.4.3.tgz", + "integrity": "sha1-dhyEArgONHt3M6BDkKdXslNYBGc=", + "dev": true + }, + "tsutils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.4.0.tgz", + "integrity": "sha1-rUzm26Dlo+2934Ymt8oEB4IYn+o=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=" + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "typescript": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.3.4.tgz", + "integrity": "sha1-PTgyGCgjHkNPKHUUlZw3qCtin0I=" + }, + "unbzip2-stream": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.4.tgz", + "integrity": "sha1-jITITVtMwo/B+fV3IDu9PLhgoWo=", + "dev": true + }, + "undefsafe": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz", + "integrity": "sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "update-notifier": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", + "integrity": "sha1-B7XcIGazYnqztPUwEw9+3doHpMw=", + "dev": true + }, + "url-join": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", + "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=" + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true + }, + "vary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=" + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=" + }, + "window-size": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", + "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", + "dev": true + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "dev": true + }, + "xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", + "integrity": "sha1-AwiOnr+edWtpdRYR0qXvWRSCyZU=", + "dev": true + }, + "yauzl": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz", + "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, + "zone.js": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz", + "integrity": "sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=" + } + } +} diff --git a/package.json b/package.json index 6a72af4..625512a 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,53 @@ { - "name": "handoffbot", - "version": "0.1.0", + "name": "botbuilder-handoff", + "version": "0.1.8", "license": "MIT", - "description": "Create example to escalate to a human", - "main": "app.js", + "description": "Bot hand off module for the Microsoft Bot Framework. It allows you to transfer a customer from talking to a bot to talking to a human.", + "main": "dist/index.js", + "homepage": "https://github.com/palindromed/Bot-HandOff/tree/npm-handoff", "scripts": { "postinstall": "tsc", + "clean": "rm -rf dist", "build": "tsc", + "rebuild": "npm run clean && npm run build", "watch": "tsc -w", - "nodewatch": "nodemon dist/app.js", - "start": "node dist/app.js", - "test": "echo \"Error: no test specified\" && exit 1" + "nodewatch": "nodemon dist/index.js", + "start": "node dist/index.js", + "buildTest": "tsc -p test/tsconfig.json", + "test": "npm run clean && npm run buildTest && node_modules/mocha/bin/_mocha --require node_modules/ts-node/register test/handoff.spec.ts" }, + "repository": { + "type": "git", + "url": "git+https://github.com/palindromed/Bot-HandOff.git" + }, + "keywords": [ + "bot", + "handover", + "hand off", + "bot framework" + ], "dependencies": { - "botbuilder": "^3.7.0", + "applicationinsights": "^0.20.1", + "bluebird": "^3.5.0", + "body-parser": "^1.17.2", + "botbuilder": "^3.8.4", + "cors": "2.8.3", "express": "^4.14.0", + "mongoose": "^4.9.7", "typescript": "^2.1.4" }, "devDependencies": { + "@types/chai": "^4.0.1", "@types/express": "^4.0.34", + "@types/mocha": "^2.2.41", + "@types/mongoose": "^4.7.11", "@types/node": "^6.0.59", - "nodemon": "^1.11.0" + "bot-tester": "0.0.30", + "chai": "^4.0.2", + "mocha": "^3.4.2", + "mongodb": "^2.2.29", + "nodemon": "^1.11.0", + "test-with-mongo": "0.0.8", + "ts-node": "^3.1.0" } } diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..3ea08a0 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,142 @@ +import * as builder from 'botbuilder'; +import { Conversation, ConversationState, Handoff } from './handoff'; +const indexExports = require('./index'); + +export function commandsMiddleware(handoff: Handoff) { + return { + botbuilder: (session: builder.Session, next: Function) => { + if (session.message.type === 'message') { + command(session, next, handoff); + } else { + // allow messages of non 'message' type through + next(); + } + } + } +} + +function command(session: builder.Session, next: Function, handoff: Handoff) { + if (handoff.isAgent(session)) { + agentCommand(session, next, handoff); + } else { + customerCommand(session, next, handoff); + } +} + +async function agentCommand( + session: builder.Session, + next: Function, + handoff: Handoff +) { + + const message = session.message; + const conversation = await handoff.getConversation({ agentConversationId: message.address.conversation.id }); + const inputWords = message.text.split(' '); + + if (inputWords.length == 0) + return; + + // Commands to execute whether connected to a customer or not + if (inputWords[0] === 'options') { + sendAgentCommandOptions(session); + return; + } else if (inputWords[0] === 'list') { + session.send(await currentConversations(handoff)); + return; + } + // Commands to execute when not connected to a customer + if (!conversation) { + switch (inputWords[0]) { + case 'connect': + const newConversation = await handoff.connectCustomerToAgent( + inputWords.length > 1 + ? { customerName: inputWords.slice(1).join(' ') } + : { bestChoice: true }, + message.address + ); + if (newConversation) { + session.send("You are connected to " + newConversation.customer.user.name); + } else { + session.send("No customers waiting."); + } + break; + default: + sendAgentCommandOptions(session); + break; + } + return; + } + + if (conversation.state !== ConversationState.Agent) { + // error state -- should not happen + session.send("Shouldn't be in this state - agent should have been cleared out."); + console.log("Shouldn't be in this state - agent should have been cleared out"); + return; + } + + if (message.text === 'disconnect') { + if (await handoff.connectCustomerToBot({ customerConversationId: conversation.customer.conversation.id })) { + session.send("Customer " + conversation.customer.user.name + " is now connected to the bot."); + } + return; + } + + next(); +} + +async function customerCommand(session: builder.Session, next: Function, handoff: Handoff) { + const message = session.message; + const customerStartHandoffCommandRegex = new RegExp("^" + indexExports._customerStartHandoffCommand + "$", "gi"); + if (customerStartHandoffCommandRegex.test(message.text)) { + // lookup the conversation (create it if one doesn't already exist) + const conversation = await handoff.getConversation({ customerConversationId: message.address.conversation.id }, message.address); + if (conversation.state == ConversationState.Bot) { + await handoff.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message); + await handoff.queueCustomerForAgent({ customerConversationId: conversation.customer.conversation.id }); + session.endConversation("Connecting you to the next available agent."); + return; + } + } + return next(); +} + +function sendAgentCommandOptions(session: builder.Session) { + const commands = ' ### Agent Options\n - Type *waiting* to connect to customer who has been waiting longest.\n - Type *connect { user name }* to connect to a specific conversation\n - Type *watch { user name }* to monitor a customer conversation\n - Type *history { user name }* to see a transcript of a given user\n - Type *list* to see a list of all current conversations.\n - Type *disconnect* while talking to a user to end a conversation.\n - Type *options* at any time to see these options again.'; + session.send(commands); + return; +} + +async function currentConversations(handoff: Handoff): Promise { + const conversations = await handoff.getCurrentConversations(); + if (conversations.length === 0) { + return "No customers are in conversation."; + } + + let text = '### Current Conversations \n'; + conversations.forEach(conversation => { + const starterText = ' - *' + conversation.customer.user.name + '*'; + switch (ConversationState[conversation.state]) { + case 'Bot': + text += starterText + ' is talking to the bot\n'; + break; + case 'Agent': + text += starterText + ' is talking to an agent\n'; + break; + case 'Waiting': + text += starterText + ' is waiting to talk to an agent\n'; + break; + case 'Watch': + text += starterText + ' is being monitored by an agent\n'; + break; + } + }); + + return text; +} + +async function disconnectCustomer(conversation: Conversation, handoff: any, session: builder.Session) { + if (await handoff.connectCustomerToBot({ customerConversationId: conversation.customer.conversation.id })) { + session.send("Customer " + conversation.customer.user.name + " is now connected to the bot."); + } + +} diff --git a/handoff.ts b/src/handoff.ts similarity index 52% rename from handoff.ts rename to src/handoff.ts index 6e6f320..012566c 100644 --- a/handoff.ts +++ b/src/handoff.ts @@ -1,6 +1,6 @@ import * as builder from 'botbuilder'; import { Express } from 'express'; -import { defaultProvider } from './provider'; +import { MongooseProvider } from './mongoose-provider'; // Options for state of a conversation // Customer talking to bot, waiting for next available agent or talking to an agent @@ -15,6 +15,8 @@ export enum ConversationState { export interface TranscriptLine { timestamp: any, from: string, + sentimentScore: number, + state: number, text: string } @@ -38,14 +40,15 @@ export interface Provider { init(); // Update - addToTranscript: (by: By, text: string) => boolean; - connectCustomerToAgent: (by: By, nextState: ConversationState, agentAddress: builder.IAddress) => Conversation; - connectCustomerToBot: (by: By) => boolean; - queueCustomerForAgent: (by: By) => boolean; + + addToTranscript: (by: By, message: builder.IMessage, from: string) => Promise; + connectCustomerToAgent: (by: By, agentAddress: builder.IAddress) => Promise; + connectCustomerToBot: (by: By) => Promise; + queueCustomerForAgent: (by: By) => Promise; // Get - getConversation: (by: By, customerAddress?: builder.IAddress) => Conversation; - currentConversations: () => Conversation[]; + getConversation: (by: By, customerAddress?: builder.IAddress) => Promise; + getCurrentConversations: () => Promise; } export class Handoff { @@ -53,7 +56,7 @@ export class Handoff { constructor( private bot: builder.UniversalBot, public isAgent: (session: builder.Session) => boolean, - private provider = defaultProvider + private provider = new MongooseProvider() ) { this.provider.init(); } @@ -64,26 +67,25 @@ export class Handoff { // Pass incoming messages to routing method if (session.message.type === 'message') { this.routeMessage(session, next); + } else { + // allow messages of non 'message' type through + next(); } }, - send: (event: builder.IEvent, next: Function) => { + send: async (event: builder.IMessage, next: Function) => { // Messages sent from the bot do not need to be routed - const message = event as builder.IMessage; - const customerConversation = this.getConversation({ customerConversationId: event.address.conversation.id }); - // send message to agent observing conversation - if (customerConversation.state === ConversationState.Watch) { - this.bot.send(new builder.Message().address(customerConversation.agent).text(message.text)); + // Not all messages from the bot are type message, we only want to record the actual messages + if (event.type === 'message' && !event.entities) { + this.transcribeMessageFromBot(event as builder.IMessage, next); + } else { + //If not a message (text), just send to user without transcribing + next(); } - this.trancribeMessageFromBot(message, next); - } } } - private routeMessage( - session: builder.Session, - next: Function - ) { + private routeMessage(session: builder.Session, next: Function) { if (this.isAgent(session)) { this.routeAgentMessage(session) } else { @@ -91,25 +93,28 @@ export class Handoff { } } - private routeAgentMessage(session: builder.Session) { + private async routeAgentMessage(session: builder.Session) { const message = session.message; - const conversation = this.getConversation({ agentConversationId: message.address.conversation.id }); - + const conversation = await this.getConversation({ agentConversationId: message.address.conversation.id }, message.address); + await this.addToTranscript({ agentConversationId: message.address.conversation.id }, message); // if the agent is not in conversation, no further routing is necessary if (!conversation) return; - // if the agent is observing a customer, no need to route message - if (conversation.state !== ConversationState.Agent) + + if (conversation.state !== ConversationState.Agent) { + // error state -- should not happen + session.send("Shouldn't be in this state - agent should have been cleared out."); return; + } // send text that agent typed to the customer they are in conversation with - this.bot.send(new builder.Message().address(conversation.customer).text(message.text)); + this.bot.send(new builder.Message().address(conversation.customer).text(message.text).addEntity({ "agent": true })); } - private routeCustomerMessage(session: builder.Session, next: Function) { + private async routeCustomerMessage(session: builder.Session, next: Function) { const message = session.message; // method will either return existing conversation or a newly created conversation if this is first time we've heard from customer - const conversation = this.getConversation({ customerConversationId: message.address.conversation.id }, message.address); - this.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message.text); + const conversation = await this.getConversation({ customerConversationId: message.address.conversation.id }, message.address); + await this.addToTranscript({ customerConversationId: conversation.customer.conversation.id }, message); switch (conversation.state) { case ConversationState.Bot: @@ -132,38 +137,33 @@ export class Handoff { } // These methods are wrappers around provider which handles data - private trancribeMessageFromBot(message: builder.IMessage, next: Function) { - this.provider.addToTranscript({ customerConversationId: message.address.conversation.id }, message.text); + private transcribeMessageFromBot(message: builder.IMessage, next: Function) { + this.provider.addToTranscript({ customerConversationId: message.address.conversation.id }, message, 'Bot'); next(); } - public getCustomerTranscript(by: By, session: builder.Session) { - const customerConversation = this.getConversation(by); - if (customerConversation) { - customerConversation.transcript.forEach(transcriptLine => - session.send(transcriptLine.text)); - } else { - session.send('No Transcript to show. Try entering a username or try again when connected to a customer'); - } + public connectCustomerToAgent = async (by: By, agentAddress: builder.IAddress): Promise => { + return await this.provider.connectCustomerToAgent(by, agentAddress); } - public connectCustomerToAgent = (by: By, nextState: ConversationState, agentAddress: builder.IAddress) => - this.provider.connectCustomerToAgent(by, nextState, agentAddress); - - public connectCustomerToBot = (by: By) => - this.provider.connectCustomerToBot(by); - - public queueCustomerForAgent = (by: By) => - this.provider.queueCustomerForAgent(by); - - public addToTranscript = (by: By, text: string) => - this.provider.addToTranscript(by, text); + public connectCustomerToBot = async (by: By): Promise => { + return await this.provider.connectCustomerToBot(by); + } - public getConversation = (by: By, customerAddress?: builder.IAddress) => - this.provider.getConversation(by, customerAddress); + public queueCustomerForAgent = async (by: By): Promise => { + return await this.provider.queueCustomerForAgent(by); + } - public currentConversations = () => - this.provider.currentConversations(); + public addToTranscript = async (by: By, message: builder.IMessage): Promise => { + let from = by.agentConversationId ? 'Agent' : 'Customer'; + return await this.provider.addToTranscript(by, message, from); + } + public getConversation = async (by: By, customerAddress?: builder.IAddress): Promise => { + return await this.provider.getConversation(by, customerAddress); + } + public getCurrentConversations = async (): Promise => { + return await this.provider.getCurrentConversations(); + } }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d749b53 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,118 @@ +import { MongooseProvider, mongoose } from './mongoose-provider'; +import { Handoff } from './handoff'; +import { commandsMiddleware } from './commands'; +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as cors from 'cors'; +let appInsights = require('applicationinsights'); + +export function setup(bot, app, isAgent, options) { + + let mongooseProvider = null; + let _retainData = null; + let _directLineSecret = null; + let _mongodbProvider = null; + let _textAnalyticsKey = null; + let _appInsightsInstrumentationKey = null; + let _customerStartHandoffCommand = null; + + const handoff = new Handoff(bot, isAgent); + + options = options || {}; + + if (!options.mongodbProvider && !process.env.MONGODB_PROVIDER) { + throw new Error('Bot-Handoff: Mongo DB Connection String was not provided in setup options (mongodbProvider) or in the environment variables (MONGODB_PROVIDER)'); + } else { + _mongodbProvider = options.mongodbProvider || process.env.MONGODB_PROVIDER; + mongooseProvider = new MongooseProvider(); + mongoose.connect(_mongodbProvider); + } + + if (!options.directlineSecret && !process.env.MICROSOFT_DIRECTLINE_SECRET) { + throw new Error('Bot-Handoff: Microsoft Bot Builder Direct Line Secret was not provided in setup options (directlineSecret) or in the environment variables (MICROSOFT_DIRECTLINE_SECRET)'); + } else { + _directLineSecret = options.directlineSecret || process.env.MICROSOFT_DIRECTLINE_SECRET; + } + + if (!options.textAnalyticsKey && !process.env.CG_SENTIMENT_KEY) { + console.warn('Bot-Handoff: Microsoft Cognitive Services Text Analytics Key was not provided in setup options (textAnalyticsKey) or in the environment variables (CG_SENTIMENT_KEY). Sentiment will not be analysed in the transcript, the score will be recorded as -1 for all text.'); + } else { + _textAnalyticsKey = options.textAnalyticsKey || process.env.CG_SENTIMENT_KEY; + exports._textAnalyticsKey = _textAnalyticsKey; + } + + if (!options.appInsightsInstrumentationKey && !process.env.APPINSIGHTS_INSTRUMENTATIONKEY) { + console.warn('Bot-Handoff: Microsoft Application Insights Instrumentation Key was not provided in setup options (appInsightsInstrumentationKey) or in the environment variables (APPINSIGHTS_INSTRUMENTATIONKEY). The conversation object will not be logged to Application Insights.'); + } else { + _appInsightsInstrumentationKey = options.appInsightsInstrumentationKey || process.env.APPINSIGHTS_INSTRUMENTATIONKEY; + appInsights.setup(_appInsightsInstrumentationKey).start(); + exports._appInsights = appInsights; + } + + if (!options.retainData && !process.env.RETAIN_DATA) { + console.warn('Bot-Handoff: Retain data value was not provided in setup options (retainData) or in the environment variables (RETAIN_DATA). Not providing this value or setting it to "false" means that if a customer speaks to an agent, the conversation record with that customer will be deleted after an agent disconnects the conversation. Set to "true" to keep all data records in the mongo database.'); + } else { + _retainData = options.retainData || process.env.RETAIN_DATA; + exports._retainData = _retainData; + } + + if (!options.customerStartHandoffCommand && !process.env.CUSTOMER_START_HANDOFF_COMMAND) { + console.warn('Bot-Handoff: The customer command to start the handoff was not provided in setup options (customerStartHandoffCommand) or in the environment variables (CUSTOMER_START_HANDOFF_COMMAND). The default command will be set to help. Regex is used on this command to make sure the activation of the handoff only works if the user types the exact phrase provided in this property.'); + _customerStartHandoffCommand = "help"; + exports._customerStartHandoffCommand = _customerStartHandoffCommand; + } else { + _customerStartHandoffCommand = options.customerStartHandoffCommand || process.env.CUSTOMER_START_HANDOFF_COMMAND; + exports._customerStartHandoffCommand = _customerStartHandoffCommand; + } + + if (bot) { + bot.use( + commandsMiddleware(handoff), + handoff.routingMiddleware(), + ) + } + + if (app && _directLineSecret != null) { + app.use(cors({ origin: '*' })); + app.use(bodyParser.json()); + + // Create endpoint for agent / call center + app.use('/webchat', express.static('public')); + + // Endpoint to get current conversations + app.get('/api/conversations', async (req, res) => { + const authHeader = req.headers['authorization']; + console.log(authHeader); + console.log(req.headers); + if (authHeader) { + if (authHeader === 'Bearer ' + _directLineSecret) { + let conversations = await mongooseProvider.getCurrentConversations() + res.status(200).send(conversations); + } + } + res.status(401).send('Not Authorized'); + }); + + // Endpoint to trigger handover + app.post('/api/conversations', async (req, res) => { + const authHeader = req.headers['authorization']; + console.log(authHeader); + console.log(req.headers); + if (authHeader) { + if (authHeader === 'Bearer ' + _directLineSecret) { + if (await handoff.queueCustomerForAgent({ customerConversationId: req.body.conversationId })) { + res.status(200).send({ "code": 200, "message": "OK" }); + } else { + res.status(400).send({ "code": 400, "message": "Can't find conversation ID" }); + } + } + } else { + res.status(401).send({ "code": 401, "message": "Not Authorized" }); + } + }); + } else { + throw new Error('Microsoft Bot Builder Direct Line Secret was not provided in options or the environment variable MICROSOFT_DIRECTLINE_SECRET'); + } +} + +module.exports = { setup } diff --git a/src/mongoose-provider.ts b/src/mongoose-provider.ts new file mode 100644 index 0000000..16490eb --- /dev/null +++ b/src/mongoose-provider.ts @@ -0,0 +1,252 @@ +import * as builder from 'botbuilder'; +import * as bluebird from 'bluebird'; +import * as request from 'request'; +import * as _ from 'lodash'; +import mongoose = require('mongoose'); +mongoose.Promise = bluebird; + +import { By, Conversation, Provider, ConversationState } from './handoff'; + +const indexExports = require('./index'); + +// ------------------- +// Bot Framework types +// ------------------- +export const IIdentitySchema = new mongoose.Schema({ + id: { type: String, required: true }, + isGroup: { type: Boolean, required: false }, + name: { type: String, required: false }, +}, { + _id: false, + strict: false, + }); + +export const IAddressSchema = new mongoose.Schema({ + bot: { type: IIdentitySchema, required: true }, + channelId: { type: String, required: true }, + conversation: { type: IIdentitySchema, required: false }, + user: { type: IIdentitySchema, required: true }, + id: { type: String, required: false }, + serviceUrl: { type: String, required: false }, + useAuth: { type: Boolean, required: false } +}, { + strict: false, + id: false, + _id: false + }); + +// ------------- +// Handoff types +// ------------- +export const TranscriptLineSchema = new mongoose.Schema({ + timestamp: {}, + from: String, + sentimentScore: Number, + state: Number, + text: String +}); + +export const ConversationSchema = new mongoose.Schema({ + customer: { type: IAddressSchema, required: true }, + agent: { type: IAddressSchema, required: false }, + state: { + type: Number, + required: true, + min: 0, + max: 3 + }, + transcript: [TranscriptLineSchema] +}); +export interface ConversationDocument extends Conversation, mongoose.Document { } +export const ConversationModel = mongoose.model('Conversation', ConversationSchema) + +export const BySchema = new mongoose.Schema({ + bestChoice: Boolean, + agentConversationId: String, + customerConversationId: String, + customerName: String +}); +export interface ByDocument extends By, mongoose.Document { } +export const ByModel = mongoose.model('By', BySchema); +export { mongoose }; +// ----------------- +// Mongoose Provider +// ----------------- +export class MongooseProvider implements Provider { + public init(): void { } + async addToTranscript(by: By, message: builder.IMessage, from: string): Promise { + let sentimentScore = -1; + let text = message.text; + let datetime = new Date().toISOString(); + const conversation: Conversation = await this.getConversation(by); + + if (!conversation) return false; + + if (from == "Customer") { + if (indexExports._textAnalyticsKey) { sentimentScore = await this.collectSentiment(text); } + datetime = message.localTimestamp ? message.localTimestamp : message.timestamp + } + + conversation.transcript.push({ + timestamp: datetime, + from: from, + sentimentScore: sentimentScore, + state: conversation.state, + text + }); + + if (indexExports._appInsights) { + // You can't log embedded json objects in application insights, so we are flattening the object to one item. + // Also, have to stringify the object so functions from mongodb don't get logged + let latestTranscriptItem = conversation.transcript.length-1; + let x = JSON.parse(JSON.stringify(conversation.transcript[latestTranscriptItem])); + x['botId'] = conversation.customer.bot.id; + x['customerId'] = conversation.customer.user.id; + x['customerName'] = conversation.customer.user.name; + x['customerChannelId'] = conversation.customer.channelId; + x['customerConversationId'] = conversation.customer.conversation.id; + if (conversation.agent) { + x['agentId'] = conversation.agent.user.id; + x['agentName'] = conversation.agent.user.name; + x['agentChannelId'] = conversation.agent.channelId; + x['agentConversationId'] = conversation.agent.conversation.id; + } + indexExports._appInsights.client.trackEvent("Transcript", x); + } + + return await this.updateConversation(conversation); + } + + async connectCustomerToAgent(by: By, agentAddress: builder.IAddress): Promise { + const conversation = await this.getConversation(by); + if (conversation) { + conversation.state = ConversationState.Agent; + conversation.agent = agentAddress; + } + const success = await this.updateConversation(conversation); + if (success) + return conversation; + else + return null; + } + + async queueCustomerForAgent(by: By): Promise { + const conversation = await this.getConversation(by); + if (!conversation) { + return false; + } else { + conversation.state = ConversationState.Waiting; + return await this.updateConversation(conversation); + } + } + + async connectCustomerToBot(by: By): Promise { + const conversation = await this.getConversation(by); + if (!conversation) { + return false; + } else { + conversation.state = ConversationState.Bot; + if (indexExports._retainData === "true") { + return await this.updateConversation(conversation); + } else { + if (conversation.agent) { + return await this.deleteConversation(conversation); + } else { + return await this.updateConversation(conversation); + } + + } + } + } + + async getConversation(by: By, customerAddress?: builder.IAddress): Promise { + if (by.customerName) { + return await ConversationModel.findOne({ 'customer.user.name': by.customerName }); + } else if (by.agentConversationId) { + const conversation = await ConversationModel.findOne({ 'agent.conversation.id': by.agentConversationId }); + if (conversation) return conversation; + else return null; + } else if (by.customerConversationId) { + let conversation: Conversation = await ConversationModel.findOne({ 'customer.conversation.id': by.customerConversationId }); + if (!conversation && customerAddress) { + conversation = await this.createConversation(customerAddress); + } + return conversation; + } + return null; + } + + async getCurrentConversations(): Promise { + let conversations; + try { + conversations = await ConversationModel.find(); + } catch (error) { + console.log('Failed loading conversations'); + console.log(error); + } + return conversations; + } + + private async createConversation(customerAddress: builder.IAddress): Promise { + return await ConversationModel.create({ + customer: customerAddress, + state: ConversationState.Bot, + transcript: [] + }); + } + + private async updateConversation(conversation: Conversation): Promise { + return new Promise((resolve, reject) => { + ConversationModel.findByIdAndUpdate((conversation as any)._id, conversation).then((error) => { + resolve(true) + }).catch((error) => { + console.log('Failed to update conversation'); + console.log(conversation as any); + resolve(false); + }); + }); + } + + private async deleteConversation(conversation: Conversation): Promise { + return new Promise((resolve) => { + ConversationModel.findByIdAndRemove((conversation as any)._id).then((error) => { + resolve(true); + }) + }); + } + + private async collectSentiment(text: string): Promise { + if (text == null || text == '') return; + let _sentimentUrl = 'https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/sentiment'; + let _sentimentId = 'bot-analytics'; + let _sentimentKey = indexExports._textAnalyticsKey; + + let options = { + url: _sentimentUrl, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Ocp-Apim-Subscription-Key': _sentimentKey + }, + json: true, + body: { + "documents": [ + { + "language": "en", + "id": _sentimentId, + "text": text + } + ] + } + }; + + return new Promise(function (resolve, reject) { + request(options, (error, response, body) => { + if (error) { reject(error); } + let result: any = _.find(body.documents, { id: _sentimentId }) || {}; + let score = result.score || null; + resolve(score); + }); + }); + } +} \ No newline at end of file diff --git a/provider.ts b/src/provider.ts similarity index 54% rename from provider.ts rename to src/provider.ts index ce546b3..449dac0 100644 --- a/provider.ts +++ b/src/provider.ts @@ -3,64 +3,72 @@ import { Provider, Conversation, By, ConversationState } from './handoff'; export let conversations: Conversation[]; -export const init = () => { +export const init = async () => { conversations = []; } +const convertToPromise = (value: T): Promise => { + return new Promise((resolve) => { return resolve(value) }); +} + // Update -const addToTranscript = (by: By, text: string) => { - const conversation = getConversation(by); + +const addToTranscript = async (by: By, message: builder.IMessage, from: string): Promise => { + const conversation = await getConversation(by); + let text = message.text; if (!conversation) - return false; + return convertToPromise(false); conversation.transcript.push({ - timestamp: Date.now(), - from: by.agentConversationId ? 'agent' : 'customer', + timestamp: message.localTimestamp, + from: by.agentConversationId ? 'Agent' : 'Customer', + sentimentScore: 1, + state: conversation.state, text }); - return true; + return convertToPromise(true); } -const connectCustomerToAgent = (by: By, stateUpdate: ConversationState, agentAddress: builder.IAddress) => { - const conversation = getConversation(by); +const connectCustomerToAgent = async (by: By, agentAddress: builder.IAddress): Promise => { + const conversation = await getConversation(by); if (conversation) { - conversation.state = stateUpdate; + conversation.state = ConversationState.Agent; conversation.agent = agentAddress; } - return conversation; + return convertToPromise(conversation); } -const queueCustomerForAgent = (by: By) => { - const conversation = getConversation(by); +const queueCustomerForAgent = async (by: By) => { + const conversation = await getConversation(by); if (!conversation) - return false; + return convertToPromise(false); conversation.state = ConversationState.Waiting; if (conversation.agent) delete conversation.agent; - return true; + return convertToPromise(true); } -const connectCustomerToBot = (by: By) => { - const conversation = getConversation(by); +const connectCustomerToBot = async (by: By) => { + const conversation = await getConversation(by); if (!conversation) - return false; + return convertToPromise(false); conversation.state = ConversationState.Bot; if (conversation.agent) delete conversation.agent; - return true; + return convertToPromise(true); } // Get -const getConversation = ( +const getConversation = async ( by: By, customerAddress?: builder.IAddress // if looking up by customerConversationId, create new conversation if one doesn't already exist -) => { +): Promise => { // local function to create a conversation if customer does not already have one const createConversation = (customerAddress: builder.IAddress) => { const conversation = { @@ -69,37 +77,37 @@ const getConversation = ( transcript: [] }; conversations.push(conversation); - return conversation; + return convertToPromise(conversation); } if (by.bestChoice) { const waitingLongest = conversations .filter(conversation => conversation.state === ConversationState.Waiting) .sort((x, y) => y.transcript[y.transcript.length - 1].timestamp - x.transcript[x.transcript.length - 1].timestamp); - return waitingLongest.length > 0 && waitingLongest[0]; + return await convertToPromise(waitingLongest.length > 0 && waitingLongest[0]); } if (by.customerName) { - return conversations.find(conversation => + return convertToPromise(conversations.find(conversation => conversation.customer.user.name == by.customerName - ); + )); } else if (by.agentConversationId) { - return conversations.find(conversation => + return convertToPromise(conversations.find(conversation => conversation.agent && conversation.agent.conversation.id === by.agentConversationId - ); + )); } else if (by.customerConversationId) { let conversation = conversations.find(conversation => conversation.customer.conversation.id === by.customerConversationId ); if (!conversation && customerAddress) { - conversation = createConversation(customerAddress); + conversation = await createConversation(customerAddress); } - return conversation; + return convertToPromise(conversation); } return null; } -const currentConversations = () => - conversations; +const getCurrentConversations = (): Promise => + convertToPromise(conversations); export const defaultProvider: Provider = { init, @@ -112,5 +120,5 @@ export const defaultProvider: Provider = { // Get getConversation, - currentConversations + getCurrentConversations, } diff --git a/test/handoff.spec.ts b/test/handoff.spec.ts new file mode 100644 index 0000000..340ad61 --- /dev/null +++ b/test/handoff.spec.ts @@ -0,0 +1,100 @@ +import { BotTester } from 'bot-tester'; +import * as builder from 'botbuilder'; +import * as Promise from 'bluebird'; +import * as chai from 'chai'; +import * as express from 'express'; +import 'mocha'; +import * as handoff from './../src'; + +const { TestWithMongo } = require('test-with-mongo'); + +const { MongoClient } = require('mongodb'); +const { expect } = chai; + +const isAgent = (session: builder.Session) => session.message.user.name.startsWith("Agent"); + +const userAddress: builder.IAddress = { channelId: 'console', + user: { id: 'user', name: 'user' }, + bot: { id: 'bot', name: 'Bot' }, + conversation: { id: 'userConversation' } +}; + +const agentAddress: builder.IAddress= { channelId: 'console', + user: { id: 'Agent', name: 'Agent' }, + bot: { id: 'bot', name: 'Bot' }, + conversation: { id: 'agentConversation' } +}; + +const MONGO_PORT = 26017; +const MONGO_CONNECTION_STRING = `mongodb://localhost:${MONGO_PORT}/test`; + +const testWithMongo = new TestWithMongo(MONGO_PORT); + +const DB_NAME = 'test'; + +describe('handoff tests', () => { + let db; + let app; + let bot; + let server; + + before(() => { + return testWithMongo.startMongoServer() + .then(() => { + const connector = new builder.ConsoleConnector(); + app = express(); + + bot = new builder.UniversalBot(connector); + + connector.listen(); + + bot.dialog('/', (session) => { + session.send('Echo ' + session.message.text) + }); + + server = app.listen(3978, '::', () => { + console.log('Server Up'); + }); + + handoff.setup(bot, app, isAgent, { + mongodbProvider: testWithMongo.getConnectionString('testdb'), + directlineSecret: 'this can be anything', + retainData: false, + customerStartHandoffCommand: 'HELP' + }); + }); + }); + + after((done) => { + server.close(() => testWithMongo.clean().then(done)); + }) + + afterEach(() => { + return testWithMongo.dropDb(DB_NAME); + }) + + it('can switch from bot to agent control', () => { + const { + executeDialogTest, + sendMessageToBot, + InspectSessionDialogStep, + SendMessageToBotDialogStep + } + = BotTester(bot, userAddress as any); + + const userMessage = new builder.Message().text('HELP').address(userAddress).toMessage(); + const connectMessage = new builder.Message().text('connect user').address(agentAddress).toMessage(); + const testSendUserMessage = new builder.Message().text('Hi there home slice!').address(agentAddress).toMessage(); + const testSendUserReceiveMessage = new builder.Message().text('Hi there home slice!').address(userAddress).toMessage(); + const testUserResponseMessage = new builder.Message().text('how are you? I am a user').address(userAddress).toMessage() + const testAgentReceiveUserResponseMessage = new builder.Message().text('how are you? I am a user').address(agentAddress).toMessage(); + + return executeDialogTest([ + new SendMessageToBotDialogStep('hey', 'Echo hey'), + new SendMessageToBotDialogStep(userMessage, 'Connecting you to the next available agent.'), + new SendMessageToBotDialogStep(connectMessage, 'You are connected to user', agentAddress), + new SendMessageToBotDialogStep(testSendUserMessage, testSendUserReceiveMessage), + new SendMessageToBotDialogStep(testUserResponseMessage, testAgentReceiveUserResponseMessage) + ]); + }); +}); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..2c05328 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": false, + "outDir": "../dist", + "sourceMap": true + }, + "files": [ + "../src/index.ts", + "./handoff.spec.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 64a7a03..edfd159 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,10 @@ "module": "commonjs", "target": "es6", "noImplicitAny": false, - "outDir": "./dist" + "outDir": "./dist", + "sourceMap": true }, "files": [ - "app.ts" + "./src/index.ts" ] }