improvement(forwarding+excel): added forwarding and improve excel read (#1136)

* added forwarding for outlook

* lint

* improved excel sheet read

* addressed greptile

* fixed bodytext getting truncated

* fixed any type

* added html func

---------

Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
Adam Gough
2025-08-26 21:18:09 -07:00
committed by GitHub
parent 861ab1446a
commit ab74b13802
12 changed files with 355 additions and 58 deletions

View File

@@ -22,6 +22,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
{ label: 'Send Email', id: 'send_outlook' }, { label: 'Send Email', id: 'send_outlook' },
{ label: 'Draft Email', id: 'draft_outlook' }, { label: 'Draft Email', id: 'draft_outlook' },
{ label: 'Read Email', id: 'read_outlook' }, { label: 'Read Email', id: 'read_outlook' },
{ label: 'Forward Email', id: 'forward_outlook' },
], ],
value: () => 'send_outlook', value: () => 'send_outlook',
}, },
@@ -51,9 +52,30 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
type: 'short-input', type: 'short-input',
layout: 'full', layout: 'full',
placeholder: 'Recipient email address', placeholder: 'Recipient email address',
condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, condition: {
field: 'operation',
value: ['send_outlook', 'draft_outlook', 'forward_outlook'],
},
required: true, required: true,
}, },
{
id: 'messageId',
title: 'Message ID',
type: 'short-input',
layout: 'full',
placeholder: 'Message ID to forward',
condition: { field: 'operation', value: ['forward_outlook'] },
required: true,
},
{
id: 'comment',
title: 'Comment',
type: 'long-input',
layout: 'full',
placeholder: 'Optional comment to include when forwarding',
condition: { field: 'operation', value: ['forward_outlook'] },
required: false,
},
{ {
id: 'subject', id: 'subject',
title: 'Subject', title: 'Subject',
@@ -157,7 +179,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
}, },
], ],
tools: { tools: {
access: ['outlook_send', 'outlook_draft', 'outlook_read'], access: ['outlook_send', 'outlook_draft', 'outlook_read', 'outlook_forward'],
config: { config: {
tool: (params) => { tool: (params) => {
switch (params.operation) { switch (params.operation) {
@@ -167,6 +189,8 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
return 'outlook_read' return 'outlook_read'
case 'draft_outlook': case 'draft_outlook':
return 'outlook_draft' return 'outlook_draft'
case 'forward_outlook':
return 'outlook_forward'
default: default:
throw new Error(`Invalid Outlook operation: ${params.operation}`) throw new Error(`Invalid Outlook operation: ${params.operation}`)
} }
@@ -197,6 +221,9 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
to: { type: 'string', description: 'Recipient email address' }, to: { type: 'string', description: 'Recipient email address' },
subject: { type: 'string', description: 'Email subject' }, subject: { type: 'string', description: 'Email subject' },
body: { type: 'string', description: 'Email content' }, body: { type: 'string', description: 'Email content' },
// Forward operation inputs
messageId: { type: 'string', description: 'Message ID to forward' },
comment: { type: 'string', description: 'Optional comment for forwarding' },
// Read operation inputs // Read operation inputs
folder: { type: 'string', description: 'Email folder' }, folder: { type: 'string', description: 'Email folder' },
manualFolder: { type: 'string', description: 'Manual folder name' }, manualFolder: { type: 'string', description: 'Manual folder name' },

View File

@@ -1,4 +1,5 @@
import { and, eq } from 'drizzle-orm' import { and, eq } from 'drizzle-orm'
import { htmlToText } from 'html-to-text'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import { createLogger } from '@/lib/logs/console/logger' import { createLogger } from '@/lib/logs/console/logger'
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis' import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
@@ -79,6 +80,24 @@ export interface OutlookWebhookPayload {
rawEmail?: OutlookEmail // Only included when includeRawEmail is true rawEmail?: OutlookEmail // Only included when includeRawEmail is true
} }
/**
* Convert HTML content to a readable plain-text representation.
* Keeps reasonable newlines and decodes common HTML entities.
*/
function convertHtmlToPlainText(html: string): string {
if (!html) return ''
return htmlToText(html, {
wordwrap: false,
selectors: [
{ selector: 'a', options: { hideLinkHrefIfSameAsText: true, noAnchorUrl: true } },
{ selector: 'img', format: 'skip' },
{ selector: 'script', format: 'skip' },
{ selector: 'style', format: 'skip' },
],
preserveNewlines: true,
})
}
export async function pollOutlookWebhooks() { export async function pollOutlookWebhooks() {
logger.info('Starting Outlook webhook polling') logger.info('Starting Outlook webhook polling')
@@ -357,7 +376,18 @@ async function processOutlookEmails(
to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '', to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '', cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
date: email.receivedDateTime, date: email.receivedDateTime,
bodyText: email.bodyPreview || '', bodyText: (() => {
const content = email.body?.content || ''
const type = (email.body?.contentType || '').toLowerCase()
if (!content) {
return email.bodyPreview || ''
}
if (type === 'text' || type === 'text/plain') {
return content
}
// Default to converting HTML or unknown types
return convertHtmlToPlainText(content)
})(),
bodyHtml: email.body?.content || '', bodyHtml: email.body?.content || '',
hasAttachments: email.hasAttachments, hasAttachments: email.hasAttachments,
isRead: email.isRead, isRead: email.isRead,

View File

@@ -89,6 +89,7 @@
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"geist": "1.4.2", "geist": "1.4.2",
"groq-sdk": "^0.15.0", "groq-sdk": "^0.15.0",
"html-to-text": "^9.0.5",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"jose": "6.0.11", "jose": "6.0.11",
@@ -133,6 +134,7 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@trigger.dev/build": "4.0.0", "@trigger.dev/build": "4.0.0",
"@types/html-to-text": "^9.0.4",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7", "@types/jsdom": "21.1.7",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",

View File

@@ -12,6 +12,61 @@ import {
const logger = createLogger('Tools') const logger = createLogger('Tools')
// Extract a concise, meaningful error message from diverse API error shapes
function getDeepApiErrorMessage(errorInfo?: {
status?: number
statusText?: string
data?: any
}): string {
return (
// GraphQL errors (Linear API)
errorInfo?.data?.errors?.[0]?.message ||
// X/Twitter API specific pattern
errorInfo?.data?.errors?.[0]?.detail ||
// Generic details array
errorInfo?.data?.details?.[0]?.message ||
// Hunter API pattern
errorInfo?.data?.errors?.[0]?.details ||
// Direct errors array (when errors[0] is a string or simple object)
(Array.isArray(errorInfo?.data?.errors)
? typeof errorInfo.data.errors[0] === 'string'
? errorInfo.data.errors[0]
: errorInfo.data.errors[0]?.message
: undefined) ||
// Notion/Discord/GitHub/Twilio pattern
errorInfo?.data?.message ||
// SOAP/XML fault patterns
errorInfo?.data?.fault?.faultstring ||
errorInfo?.data?.faultstring ||
// Microsoft/OAuth error descriptions
errorInfo?.data?.error_description ||
// Airtable/Google fallback pattern
(typeof errorInfo?.data?.error === 'object'
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
: errorInfo?.data?.error) ||
// HTTP status text fallback
errorInfo?.statusText ||
// Final fallback
`Request failed with status ${errorInfo?.status || 'unknown'}`
)
}
// Create an Error instance from errorInfo and attach useful context
function createTransformedErrorFromErrorInfo(errorInfo?: {
status?: number
statusText?: string
data?: any
}): Error {
const message = getDeepApiErrorMessage(errorInfo)
const transformed = new Error(message)
Object.assign(transformed, {
status: errorInfo?.status,
statusText: errorInfo?.statusText,
data: errorInfo?.data,
})
return transformed
}
/** /**
* Process file outputs for a tool result if execution context is available * Process file outputs for a tool result if execution context is available
* Uses dynamic imports to avoid client-side bundling issues * Uses dynamic imports to avoid client-side bundling issues
@@ -410,15 +465,46 @@ async function handleInternalRequest(
const response = await fetch(fullUrl, requestOptions) const response = await fetch(fullUrl, requestOptions)
// Parse response data once // For non-OK responses, attempt JSON first; if parsing fails, preserve legacy error expected by tests
if (!response.ok) {
let errorData: any
try {
errorData = await response.json()
} catch (jsonError) {
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
})
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
}
const { isError, errorInfo } = isErrorResponse(response, errorData)
if (isError) {
const errorToTransform = createTransformedErrorFromErrorInfo(errorInfo)
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
status: errorInfo?.status,
errorData: errorInfo?.data,
})
throw errorToTransform
}
}
// Parse response data once with guard for empty 202 bodies
let responseData let responseData
try { const status = response.status
responseData = await response.json() if (status === 202) {
} catch (jsonError) { // Many APIs (e.g., Microsoft Graph) return 202 with empty body
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, { responseData = { status }
error: jsonError instanceof Error ? jsonError.message : String(jsonError), } else {
}) try {
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`) responseData = await response.json()
} catch (jsonError) {
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
})
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
}
} }
// Check for error conditions // Check for error conditions
@@ -426,44 +512,7 @@ async function handleInternalRequest(
if (isError) { if (isError) {
// Handle error case // Handle error case
const errorToTransform = new Error( const errorToTransform = createTransformedErrorFromErrorInfo(errorInfo)
// GraphQL errors (Linear API)
errorInfo?.data?.errors?.[0]?.message ||
// X/Twitter API specific pattern
errorInfo?.data?.errors?.[0]?.detail ||
// Generic details array
errorInfo?.data?.details?.[0]?.message ||
// Hunter API pattern
errorInfo?.data?.errors?.[0]?.details ||
// Direct errors array (when errors[0] is a string or simple object)
(Array.isArray(errorInfo?.data?.errors)
? typeof errorInfo.data.errors[0] === 'string'
? errorInfo.data.errors[0]
: errorInfo.data.errors[0]?.message
: undefined) ||
// Notion/Discord/GitHub/Twilio pattern
errorInfo?.data?.message ||
// SOAP/XML fault patterns
errorInfo?.data?.fault?.faultstring ||
errorInfo?.data?.faultstring ||
// Microsoft/OAuth error descriptions
errorInfo?.data?.error_description ||
// Airtable/Google fallback pattern
(typeof errorInfo?.data?.error === 'object'
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
: errorInfo?.data?.error) ||
// HTTP status text fallback
errorInfo?.statusText ||
// Final fallback
`Request failed with status ${errorInfo?.status || 'unknown'}`
)
// Add error context
Object.assign(errorToTransform, {
status: errorInfo?.status,
statusText: errorInfo?.statusText,
data: errorInfo?.data,
})
logger.error(`[${requestId}] Internal API error for ${toolId}:`, { logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
status: errorInfo?.status, status: errorInfo?.status,

View File

@@ -35,7 +35,8 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
type: 'string', type: 'string',
required: false, required: false,
visibility: 'user-or-llm', visibility: 'user-or-llm',
description: 'The range of cells to read from', description:
'The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet.',
}, },
}, },
@@ -53,10 +54,19 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
} }
const rangeInput = params.range.trim() const rangeInput = params.range.trim()
// If the input contains no '!', treat it as a sheet name only and fetch usedRange
if (!rangeInput.includes('!')) {
const sheetOnly = encodeURIComponent(rangeInput)
return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${sheetOnly}')/usedRange(valuesOnly=true)`
}
const match = rangeInput.match(/^([^!]+)!(.+)$/) const match = rangeInput.match(/^([^!]+)!(.+)$/)
if (!match) { if (!match) {
throw new Error(`Invalid range format: "${params.range}". Use the format "Sheet1!A1:B2"`) throw new Error(
`Invalid range format: "${params.range}". Use "Sheet1!A1:B2" or just "Sheet1" to read the whole sheet`
)
} }
const sheetName = encodeURIComponent(match[1]) const sheetName = encodeURIComponent(match[1])
@@ -104,7 +114,7 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
if (!rangeResp.ok) { if (!rangeResp.ok) {
// Normalize Microsoft Graph sheet/range errors to a friendly message // Normalize Microsoft Graph sheet/range errors to a friendly message
throw new Error( throw new Error(
'Invalid range provided or worksheet not found. Provide a range like "Sheet1!A1:B2"' 'Invalid range provided or worksheet not found. Provide a range like "Sheet1!A1:B2" or just the sheet name to read the whole sheet'
) )
} }

View File

@@ -0,0 +1,154 @@
import type { OutlookForwardParams, OutlookForwardResponse } from '@/tools/outlook/types'
import type { ToolConfig } from '@/tools/types'
export const outlookForwardTool: ToolConfig<OutlookForwardParams, OutlookForwardResponse> = {
id: 'outlook_forward',
name: 'Outlook Forward',
description: 'Forward an existing Outlook message to specified recipients',
version: '1.0.0',
oauth: {
required: true,
provider: 'outlook',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Outlook',
},
messageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the message to forward',
},
to: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Recipient email address(es), comma-separated',
},
comment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional comment to include with the forwarded message',
},
},
request: {
url: (params) => {
return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward`
},
method: 'POST',
headers: (params) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}
},
body: (params: OutlookForwardParams): Record<string, any> => {
const parseEmails = (emailString?: string) => {
if (!emailString) return []
return emailString
.split(',')
.map((email) => email.trim())
.filter((email) => email.length > 0)
.map((email) => ({ emailAddress: { address: email } }))
}
const toRecipients = parseEmails(params.to)
if (toRecipients.length === 0) {
throw new Error('At least one recipient is required to forward a message')
}
return {
comment: params.comment ?? '',
toRecipients,
}
},
},
transformResponse: async (response: Response) => {
const status = response.status
const requestId =
response.headers?.get('request-id') || response.headers?.get('x-ms-request-id') || undefined
// Graph forward action typically returns 202/204 with no body. Try to read text safely.
let bodyText = ''
try {
bodyText = await response.text()
} catch (_) {
// ignore body read errors
}
// Attempt to parse JSON if present (rare for this endpoint). Extract message identifiers if available.
let parsed: any | undefined
if (bodyText && bodyText.trim().length > 0) {
try {
parsed = JSON.parse(bodyText)
} catch (_) {
// non-JSON body; ignore
}
}
const messageId = parsed?.id || parsed?.messageId || parsed?.internetMessageId
const internetMessageId = parsed?.internetMessageId
return {
success: true,
output: {
message:
status === 202 || status === 204
? 'Email forwarded successfully'
: `Email forwarded (HTTP ${status})`,
results: {
status: 'forwarded',
timestamp: new Date().toISOString(),
httpStatus: status,
requestId,
...(messageId ? { messageId } : {}),
...(internetMessageId ? { internetMessageId } : {}),
},
},
}
},
outputs: {
message: { type: 'string', description: 'Success or error message' },
results: {
type: 'object',
description: 'Delivery result details',
properties: {
status: { type: 'string', description: 'Delivery status of the email' },
timestamp: { type: 'string', description: 'Timestamp when email was forwarded' },
httpStatus: {
type: 'number',
description: 'HTTP status code returned by the API',
optional: true,
},
requestId: {
type: 'string',
description: 'Microsoft Graph request-id header for tracing',
optional: true,
},
messageId: {
type: 'string',
description: 'Forwarded message ID if provided by API',
optional: true,
},
internetMessageId: {
type: 'string',
description: 'RFC 822 Message-ID if provided',
optional: true,
},
},
},
},
}

View File

@@ -1,5 +1,6 @@
import { outlookDraftTool } from '@/tools/outlook/draft' import { outlookDraftTool } from '@/tools/outlook/draft'
import { outlookForwardTool } from '@/tools/outlook/forward'
import { outlookReadTool } from '@/tools/outlook/read' import { outlookReadTool } from '@/tools/outlook/read'
import { outlookSendTool } from '@/tools/outlook/send' import { outlookSendTool } from '@/tools/outlook/send'
export { outlookDraftTool, outlookReadTool, outlookSendTool } export { outlookDraftTool, outlookForwardTool, outlookReadTool, outlookSendTool }

View File

@@ -127,9 +127,7 @@ export const outlookReadTool: ToolConfig<OutlookReadParams, OutlookReadResponse>
}, },
outputs: { outputs: {
success: { type: 'boolean', description: 'Email read operation success status' },
messageCount: { type: 'number', description: 'Number of emails retrieved' },
messages: { type: 'array', description: 'Array of email message objects' },
message: { type: 'string', description: 'Success or status message' }, message: { type: 'string', description: 'Success or status message' },
results: { type: 'array', description: 'Array of email message objects' },
}, },
} }

View File

@@ -136,3 +136,19 @@ export interface CleanedOutlookMessage {
} }
export type OutlookResponse = OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse export type OutlookResponse = OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse
export interface OutlookForwardParams {
accessToken: string
messageId: string
to: string
comment?: string
}
export interface OutlookForwardResponse extends ToolResponse {
output: {
message: string
results: any
}
}
export type OutlookExtendedResponse = OutlookResponse | OutlookForwardResponse

View File

@@ -109,7 +109,12 @@ import {
} from '@/tools/notion' } from '@/tools/notion'
import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive' import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive'
import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai'
import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' import {
outlookDraftTool,
outlookForwardTool,
outlookReadTool,
outlookSendTool,
} from '@/tools/outlook'
import { parallelSearchTool } from '@/tools/parallel' import { parallelSearchTool } from '@/tools/parallel'
import { perplexityChatTool } from '@/tools/perplexity' import { perplexityChatTool } from '@/tools/perplexity'
import { import {
@@ -302,6 +307,7 @@ export const tools: Record<string, ToolConfig> = {
outlook_read: outlookReadTool, outlook_read: outlookReadTool,
outlook_send: outlookSendTool, outlook_send: outlookSendTool,
outlook_draft: outlookDraftTool, outlook_draft: outlookDraftTool,
outlook_forward: outlookForwardTool,
linear_read_issues: linearReadIssuesTool, linear_read_issues: linearReadIssuesTool,
linear_create_issue: linearCreateIssueTool, linear_create_issue: linearCreateIssueTool,
onedrive_create_folder: onedriveCreateFolderTool, onedrive_create_folder: onedriveCreateFolderTool,

View File

@@ -79,7 +79,7 @@ export const outlookPollingTrigger: TriggerConfig = {
}, },
bodyText: { bodyText: {
type: 'string', type: 'string',
description: 'Plain text email body (preview)', description: 'Plain text email body',
}, },
bodyHtml: { bodyHtml: {
type: 'string', type: 'string',

View File

@@ -118,6 +118,7 @@
"fuse.js": "7.1.0", "fuse.js": "7.1.0",
"geist": "1.4.2", "geist": "1.4.2",
"groq-sdk": "^0.15.0", "groq-sdk": "^0.15.0",
"html-to-text": "^9.0.5",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"ioredis": "^5.6.0", "ioredis": "^5.6.0",
"jose": "6.0.11", "jose": "6.0.11",
@@ -162,6 +163,7 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@trigger.dev/build": "4.0.0", "@trigger.dev/build": "4.0.0",
"@types/html-to-text": "^9.0.4",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.7", "@types/jsdom": "21.1.7",
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
@@ -1378,6 +1380,8 @@
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/html-to-text": ["@types/html-to-text@9.0.4", "", {}, "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ=="],
"@types/inquirer": ["@types/inquirer@8.2.12", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ=="], "@types/inquirer": ["@types/inquirer@8.2.12", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ=="],
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],