commit 04f1d7d9d1edba65c4a9c900831a9a2c5713ef62 Author: Alexander Rossmanith Date: Mon Jul 20 13:20:15 2026 +0530 feat: initial commit for Alexa Malayalam Calendar skill diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16651de --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.js + +# Build and Deployment Output +dist/ +build/ +*.zip +*.tar.gz + +# Environment & Local Secrets +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor & OS files +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Local Testing / Coverage +coverage/ +.nyc_output/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d156235 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Alexa Malayalam Calendar + +## Testing + +``` + node test/test-local.mjs + or + npm test +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bf46fe4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,52 @@ +{ + "name": "alexamalayalamcalendar", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "alexamalayalamcalendar", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ask-sdk-core": "^2.14.0", + "kollavarsham": "^2.5.4" + } + }, + "node_modules/ask-sdk-core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.14.0.tgz", + "integrity": "sha512-G2yEKbY+XYTFzJXGRpsg7xJHqqgBcgnKEgklXUMRWRFv10gwHgtWDLLlBV6h4IdysTx782rCVJK/UcHcaGbEpA==", + "license": "Apache-2.0", + "dependencies": { + "ask-sdk-runtime": "^2.14.0" + }, + "peerDependencies": { + "ask-sdk-model": "^1.29.0" + } + }, + "node_modules/ask-sdk-model": { + "version": "1.86.0", + "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.86.0.tgz", + "integrity": "sha512-JmC5mypPBz5Q1Yx1WyeAr2Q/z2Cjm98EjLjTlYAolXF4gokU7fDDjeOAyAD5dkyHjyGLeCEvlC0MJYWFwc84dw==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/ask-sdk-runtime": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/ask-sdk-runtime/-/ask-sdk-runtime-2.14.0.tgz", + "integrity": "sha512-a96pPs1RU3GgXBHplqAVqh2uxEuSYjTD5+XSbHsf6Fz4KhHpgaxJogOIIybjA6O/d1B9sG+6bjHJGzxBJEcccA==", + "license": "Apache-2.0" + }, + "node_modules/kollavarsham": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/kollavarsham/-/kollavarsham-2.5.4.tgz", + "integrity": "sha512-0awQqPiG/6YV8exi1oBsKeliNPwpXoOsJCBKRH608pCACsbMw52tpxPJR5IBTtsqAttCY5MgJYv8olew1doWGw==", + "license": "MIT", + "engines": { + "node": ">= 16", + "npm": ">= 8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b21ffe --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "malayalam-calendar-alexa-skill", + "version": "1.0.0", + "type": "module", + "main": "src/index.mjs", + "scripts": { + "start": "node src/index.mjs", + "test": "node tests/test-local.mjs", + "build": "powershell -ExecutionPolicy Bypass -File ./scripts/build.ps1", + "deploy": "powershell -ExecutionPolicy Bypass -File ./scripts/deploy.ps1" + }, + "dependencies": { + "ask-sdk-core": "^2.14.0", + "kollavarsham": "^2.5.4" + } +} \ No newline at end of file diff --git a/scripts/build.ps1 b/scripts/build.ps1 new file mode 100644 index 0000000..35f55be --- /dev/null +++ b/scripts/build.ps1 @@ -0,0 +1,15 @@ +$ProjectRoot = (Resolve-Path "$PSScriptRoot\..").Path +$DistPath = Join-Path -Path $ProjectRoot -ChildPath "dist" +$ZipPath = Join-Path -Path $DistPath -ChildPath "bundle.zip" + +if (Test-Path -Path $DistPath) { + Remove-Item -Path $DistPath -Recurse -Force +} +New-Item -ItemType Directory -Path $DistPath | Out-Null + +Write-Host "Packaging skill into $ZipPath..." -ForegroundColor Cyan + +Set-Location -Path $ProjectRoot +Compress-Archive -Path "src", "node_modules", "package.json" -DestinationPath $ZipPath -Force + +Write-Host "Build complete. Bundle created at $ZipPath" -ForegroundColor Green diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 new file mode 100644 index 0000000..dd77b95 --- /dev/null +++ b/scripts/deploy.ps1 @@ -0,0 +1,27 @@ +param ( + [string]$FunctionName = "AlexaMalayalamCalendar" +) + +$ProjectRoot = (Resolve-Path "$PSScriptRoot\..").Path +$DistPath = Join-Path -Path $ProjectRoot -ChildPath "dist" +$BuildScript = Join-Path -Path $PSScriptRoot -ChildPath "build.ps1" +$ZipPath = Join-Path -Path $DistPath -ChildPath "bundle.zip" + +# 1. Run build script first to ensure dist/bundle.zip is fresh +Write-Host "Triggering build..." -ForegroundColor Cyan +powershell -ExecutionPolicy Bypass -File $BuildScript + +if (-not (Test-Path -Path $ZipPath)) { + Write-Host "Build failed! $ZipPath not found." -ForegroundColor Red + exit 1 +} + +# 2. Upload to AWS Lambda +Write-Host "Deploying $ZipPath to AWS Lambda ($FunctionName)..." -ForegroundColor Cyan +aws lambda update-function-code ` + --function-name $FunctionName ` + --zip-file fileb://$ZipPath ` + --no-cli-pager ` + | Out-Null + +Write-Host "Deployment finished successfully." -ForegroundColor Green diff --git a/src/index.mjs b/src/index.mjs new file mode 100644 index 0000000..cdf5c68 --- /dev/null +++ b/src/index.mjs @@ -0,0 +1,157 @@ +import Alexa from 'ask-sdk-core'; +import { Kollavarsham } from 'kollavarsham'; + +// 1. Core Logic: Handles explicit date/star question requests +const GetMalayalamDateIntentHandler = { + canHandle(handlerInput) { + return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' + && Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetMalayalamDateIntent'; + }, + handle(handlerInput) { + return generateCalendarResponse(handlerInput); + } +}; + +// 2. Fallback Entry: Handles "Alexa, open Malayalam Calendar" +const LaunchRequestHandler = { + canHandle(handlerInput) { + return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; + }, + handle(handlerInput) { + return generateCalendarResponse(handlerInput); + } +}; + +// Built-in Alexa Handlers +const HelpIntentHandler = { + canHandle(handlerInput) { + return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' + && Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent'; + }, + handle(handlerInput) { + const speechText = "You can ask me for today's Malayalam date or star."; + return handlerInput.responseBuilder + .speak(speechText) + .reprompt(speechText) + .getResponse(); + } +}; + +const CancelAndStopIntentHandler = { + canHandle(handlerInput) { + return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' + && (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent' + || Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent'); + }, + handle(handlerInput) { + const speechText = 'Goodbye!'; + return handlerInput.responseBuilder + .speak(speechText) + .getResponse(); + } +}; + +const SessionEndedRequestHandler = { + canHandle(handlerInput) { + return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest'; + }, + handle(handlerInput) { + return handlerInput.responseBuilder.getResponse(); + } +}; + +const ErrorHandler = { + canHandle() { + return true; + }, + handle(handlerInput, error) { + console.error(`Error handled: ${error.stack}`); + const speechText = 'Sorry, I had trouble understanding that. Please try again.'; + return handlerInput.responseBuilder + .speak(speechText) + .reprompt(speechText) + .getResponse(); + } +}; + +// Get current gregorian date according to Indian Standard Time (IST) +function getIstDate() { + // Construct date string formatted explicitly for India timezone + const now = new Date(); + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone: 'Asia/Kolkata', + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + + // Formats into MM/DD/YYYY in IST + const [{ value: month }, , { value: day }, , { value: year }] = formatter.formatToParts(now); + + // Create UTC date anchored at noon for that IST date + return new Date(Date.UTC(year, month - 1, day, 12, 0, 0)); +} + +// Kollavarsham calculation. +function getMalayalamDate(gregorianDate) { + // Initialize Kollavarsham with Surya Siddhanta system + // and default coordinates of Kollarvarsham project documentation. + const kollavarsham = new Kollavarsham({ + system: 'SuryaSiddhanta', + latitude: 10, + longitude: 76.2 + }); + return kollavarsham.fromGregorianDate(gregorianDate); +} + +/** + * Converts a number to its ordinal string representation (1st, 2nd, 3rd, 4th, etc.) + * @param {number} day - The day number (1-31) + * @returns {string} - Ordinal string (e.g., "4th") + */ +function getOrdinalDay(day) { + const suffixes = ['th', 'st', 'nd', 'rd']; + const remainder = day % 100; + + // Handles special cases 11th, 12th, 13th + const suffix = (remainder >= 11 && remainder <= 13) + ? 'th' + : (suffixes[day % 10] || 'th'); + + return `${day}${suffix}`; +} + +// 3. Central Response Builder +function generateCalendarResponse(handlerInput) { + const kollavarshamDate = getMalayalamDate(getIstDate()); + + // Property mappings for kollavarsham package: + const dayNum = kollavarshamDate.date; // Numeric date (e.g. 4) + const month = kollavarshamDate.masaName; // Transliterated English month (e.g. "Karkidakam") + const star = kollavarshamDate.naksatraName; // Transliterated English Nakshatra (e.g. "Uthram") + const year = kollavarshamDate.year; // Malayalam Year (e.g. 1201) + + const speechText = `Today it is the ${getOrdinalDay(dayNum)} of ${month}. Today's nakshatra is ${star}.`; + + return handlerInput.responseBuilder + .speak(speechText) + .withSimpleCard('Malayalam Calendar', speechText) + .getResponse(); +} + +// 4. Construct Skill Instance +const skill = Alexa.SkillBuilders.custom() + .addRequestHandlers( + LaunchRequestHandler, + GetMalayalamDateIntentHandler, + HelpIntentHandler, + CancelAndStopIntentHandler, + SessionEndedRequestHandler + ) + .addErrorHandlers(ErrorHandler) + .create(); + +// 5. Modern Async Lambda Handler for Node.js 24+ +export const handler = async (event, context) => { + return await skill.invoke(event, context); +}; \ No newline at end of file diff --git a/tests/test-local.mjs b/tests/test-local.mjs new file mode 100644 index 0000000..5769864 --- /dev/null +++ b/tests/test-local.mjs @@ -0,0 +1,45 @@ +import { handler } from '../src/index.mjs'; + +// Mock Alexa LaunchRequest Payload +const mockAlexaLaunchRequest = { + version: '1.0', + session: { + new: true, + sessionId: 'amzn1.echo-sdk-s-session-mock-id', + application: { + applicationId: 'amzn1.ask.skill.53645cde-742f-499e-83d3-07ed537bce39' + }, + user: { + userId: 'amzn1.ask.account.mockUser' + } + }, + context: { + System: { + application: { + applicationId: 'amzn1.ask.skill.53645cde-742f-499e-83d3-07ed537bce39' + }, + user: { + userId: 'amzn1.ask.account.mockUser' + } + } + }, + request: { + type: 'LaunchRequest', + requestId: 'amzn1.echo-api.request.mockRequestId', + timestamp: new Date().toISOString(), + locale: 'en-US' + } +}; + +async function runLocalTest() { + console.log('🚀 Running Alexa Handler Locally...\n'); + try { + const response = await handler(mockAlexaLaunchRequest, {}); + console.log('✅ Response Received:\n'); + console.log(JSON.stringify(response, null, 2)); + } catch (error) { + console.error('❌ Error executing handler locally:', error); + } +} + +runLocalTest();