@ulthon/ul-opencode-event 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,6 +135,58 @@ You can configure multiple channels, and all enabled channels will receive notif
135
135
  }
136
136
  ```
137
137
 
138
+ ## CLI Tool
139
+
140
+ This package includes a CLI tool to test your notification channel configuration before running OpenCode.
141
+
142
+ ### Test Command
143
+
144
+ ```bash
145
+ # Test all channels (validates config, tests connection, sends test email)
146
+ npx @ulthon/ul-opencode-event test
147
+
148
+ # Test a specific channel by name
149
+ npx @ulthon/ul-opencode-event test --channel "My Email"
150
+
151
+ # Only verify connection without sending email
152
+ npx @ulthon/ul-opencode-event test --no-send
153
+ ```
154
+
155
+ ### What the test command does:
156
+
157
+ 1. **Configuration validation**: Checks if the config file exists and has valid format
158
+ 2. **SMTP connection test**: Verifies that the SMTP server is reachable and credentials work
159
+ 3. **Test email**: Sends a test email to configured recipients (unless `--no-send` is used)
160
+
161
+ ### Test output example:
162
+
163
+ ```
164
+ [ul-opencode-event-cli] Starting channel test...
165
+ [ul-opencode-event-cli] Loaded project config: ./ul-opencode-event.json
166
+ [ul-opencode-event-cli] Found 1 channel(s) to test
167
+
168
+ [ul-opencode-event-cli] Testing channel: "My Email"
169
+ Type: smtp
170
+ Recipients: user@example.com
171
+
172
+ [1/3] Validating configuration...
173
+ [OK] Configuration is valid
174
+
175
+ [2/3] Testing SMTP connection to smtp.example.com:465...
176
+ [OK] Connection successful
177
+
178
+ [3/3] Sending test email...
179
+ [OK] Test email sent successfully
180
+ Message ID: <xxx>
181
+
182
+ [SUCCESS] Channel "My Email" passed all tests
183
+ Please check the inbox of: user@example.com
184
+
185
+ [ul-opencode-event-cli] Test Summary:
186
+ Passed: 1
187
+ Failed: 0
188
+ ```
189
+
138
190
  ## Troubleshooting
139
191
 
140
192
  ### No notifications sent
@@ -142,6 +194,14 @@ You can configure multiple channels, and all enabled channels will receive notif
142
194
  1. Check if `enabled: true` is set
143
195
  2. Check if the event type is enabled in `events`
144
196
  3. Check SMTP credentials are correct
197
+ 4. Ensure plugin config file exists at `.opencode/ul-opencode-event.json`, `./ul-opencode-event.json`, or `~/.config/opencode/ul-opencode-event.json`
198
+ 5. Enable debug logs with environment variable `UL_OPENCODE_EVENT_DEBUG=1`
199
+
200
+ ### How to view logs
201
+
202
+ - Start OpenCode from terminal to view stdout/stderr logs
203
+ - Plugin logs use prefix `[ul-opencode-event]`
204
+ - You will see errors for JSON parse failure, invalid config shape, and event handling failures
145
205
 
146
206
  ### SMTP authentication failed
147
207
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool for testing notification channel configuration
4
+ *
5
+ * Usage:
6
+ * npx @ulthon/ul-opencode-event test
7
+ * npx @ulthon/ul-opencode-event test --channel "My Channel"
8
+ * npx @ulthon/ul-opencode-event test --no-send # Only verify connection, don't send email
9
+ */
10
+ import * as nodemailer from 'nodemailer';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as os from 'os';
14
+ const CLI_PREFIX = '[ul-opencode-event-cli]';
15
+ /**
16
+ * Print usage information
17
+ */
18
+ function printUsage() {
19
+ console.log(`
20
+ Usage: npx @ulthon/ul-opencode-event test [options]
21
+
22
+ Options:
23
+ --channel <name> Test only the channel with this name
24
+ --no-send Only verify connection, don't send test email
25
+ --help Show this help message
26
+
27
+ Examples:
28
+ npx @ulthon/ul-opencode-event test
29
+ npx @ulthon/ul-opencode-event test --channel "Primary Email"
30
+ npx @ulthon/ul-opencode-event test --no-send
31
+ `);
32
+ }
33
+ /**
34
+ * Parse command line arguments
35
+ */
36
+ function parseArgs() {
37
+ const args = process.argv.slice(2);
38
+ const options = {
39
+ noSend: false,
40
+ };
41
+ let command = null;
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+ if (arg === '--help' || arg === '-h') {
45
+ printUsage();
46
+ process.exit(0);
47
+ }
48
+ else if (arg === '--channel') {
49
+ options.channelName = args[++i];
50
+ }
51
+ else if (arg === '--no-send') {
52
+ options.noSend = true;
53
+ }
54
+ else if (!arg.startsWith('-')) {
55
+ command = arg;
56
+ }
57
+ }
58
+ return { command, options };
59
+ }
60
+ /**
61
+ * Validate channel configuration
62
+ */
63
+ function validateChannel(channel) {
64
+ const errors = [];
65
+ // Check type
66
+ if (channel.type !== 'smtp') {
67
+ errors.push(`Unsupported channel type: "${channel.type}". Only "smtp" is supported.`);
68
+ return { valid: false, errors };
69
+ }
70
+ // Check enabled
71
+ if (channel.enabled === false) {
72
+ errors.push('Channel is disabled (enabled: false)');
73
+ return { valid: false, errors };
74
+ }
75
+ // Check recipients
76
+ if (!channel.recipients || channel.recipients.length === 0) {
77
+ errors.push('No recipients configured');
78
+ return { valid: false, errors };
79
+ }
80
+ // Check SMTP config
81
+ const config = channel.config;
82
+ if (!config.host) {
83
+ errors.push('SMTP host is missing');
84
+ }
85
+ if (!config.port) {
86
+ errors.push('SMTP port is missing');
87
+ }
88
+ if (!config.auth?.user) {
89
+ errors.push('SMTP auth.user is missing');
90
+ }
91
+ if (!config.auth?.pass) {
92
+ errors.push('SMTP auth.pass is missing');
93
+ }
94
+ return { valid: errors.length === 0, errors };
95
+ }
96
+ /**
97
+ * Create SMTP transporter
98
+ */
99
+ function createTransporter(channel) {
100
+ const config = channel.config;
101
+ return nodemailer.createTransport({
102
+ host: config.host,
103
+ port: config.port,
104
+ secure: config.secure ?? (config.port === 465),
105
+ auth: {
106
+ user: config.auth.user,
107
+ pass: config.auth.pass,
108
+ },
109
+ });
110
+ }
111
+ /**
112
+ * Test SMTP connection
113
+ */
114
+ async function testConnection(channel) {
115
+ try {
116
+ const transporter = createTransporter(channel);
117
+ await transporter.verify();
118
+ return { success: true };
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ return { success: false, error: message };
123
+ }
124
+ }
125
+ /**
126
+ * Send test email
127
+ */
128
+ async function sendTestEmail(channel) {
129
+ try {
130
+ const config = channel.config;
131
+ const transporter = createTransporter(channel);
132
+ const fromName = channel.name || 'OpenCode Notification Test';
133
+ const timestamp = new Date().toISOString();
134
+ const result = await transporter.sendMail({
135
+ from: `"${fromName}" <${config.auth.user}>`,
136
+ to: channel.recipients.join(', '),
137
+ subject: `[Test] OpenCode Notification Channel Test - ${timestamp}`,
138
+ text: `This is a test email from OpenCode notification plugin.
139
+
140
+ Channel: ${channel.name || 'unnamed'}
141
+ Type: ${channel.type}
142
+ Time: ${timestamp}
143
+
144
+ If you received this email, your notification channel is configured correctly.`,
145
+ html: `
146
+ <h2>OpenCode Notification Channel Test</h2>
147
+ <p>This is a test email from OpenCode notification plugin.</p>
148
+ <hr>
149
+ <ul>
150
+ <li><strong>Channel:</strong> ${channel.name || 'unnamed'}</li>
151
+ <li><strong>Type:</strong> ${channel.type}</li>
152
+ <li><strong>Time:</strong> ${timestamp}</li>
153
+ </ul>
154
+ <hr>
155
+ <p style="color: green;">If you received this email, your notification channel is configured correctly.</p>
156
+ `,
157
+ });
158
+ return { success: true, messageId: result.messageId };
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ return { success: false, error: message };
163
+ }
164
+ }
165
+ /**
166
+ * Test a single channel
167
+ */
168
+ async function testChannel(channel, options) {
169
+ const channelName = channel.name || 'unnamed';
170
+ console.log(`\n${CLI_PREFIX} Testing channel: "${channelName}"`);
171
+ console.log(` Type: ${channel.type}`);
172
+ console.log(` Recipients: ${channel.recipients?.join(', ') || 'none'}`);
173
+ // Step 1: Validate configuration
174
+ console.log(`\n [1/3] Validating configuration...`);
175
+ const validation = validateChannel(channel);
176
+ if (!validation.valid) {
177
+ console.log(` [FAIL] Configuration validation failed:`);
178
+ validation.errors.forEach(err => console.log(` - ${err}`));
179
+ return false;
180
+ }
181
+ console.log(` [OK] Configuration is valid`);
182
+ // Step 2: Test connection
183
+ const smtpConfig = channel.config;
184
+ console.log(`\n [2/3] Testing SMTP connection to ${smtpConfig.host}:${smtpConfig.port}...`);
185
+ const connectionResult = await testConnection(channel);
186
+ if (!connectionResult.success) {
187
+ console.log(` [FAIL] Connection failed: ${connectionResult.error}`);
188
+ return false;
189
+ }
190
+ console.log(` [OK] Connection successful`);
191
+ // Step 3: Send test email (if not skipped)
192
+ if (options.noSend) {
193
+ console.log(`\n [3/3] Skipping test email (--no-send flag)`);
194
+ console.log(`\n [SUCCESS] Channel "${channelName}" passed all tests (connection only)`);
195
+ return true;
196
+ }
197
+ console.log(`\n [3/3] Sending test email...`);
198
+ const sendResult = await sendTestEmail(channel);
199
+ if (!sendResult.success) {
200
+ console.log(` [FAIL] Failed to send test email: ${sendResult.error}`);
201
+ return false;
202
+ }
203
+ console.log(` [OK] Test email sent successfully`);
204
+ console.log(` Message ID: ${sendResult.messageId}`);
205
+ console.log(`\n [SUCCESS] Channel "${channelName}" passed all tests`);
206
+ console.log(` Please check the inbox of: ${channel.recipients?.join(', ')}`);
207
+ return true;
208
+ }
209
+ /**
210
+ * Load configuration (simplified version that loads from file)
211
+ * This is similar to config.ts but returns all channels including disabled ones
212
+ */
213
+ function loadConfigForTesting() {
214
+ // Config file paths
215
+ const globalPath = path.join(os.homedir(), '.config', 'opencode', 'ul-opencode-event.json');
216
+ const projectOpencodePath = path.join(process.cwd(), '.opencode', 'ul-opencode-event.json');
217
+ const projectLocalPath = path.join(process.cwd(), 'ul-opencode-event.json');
218
+ let globalConfig = null;
219
+ let projectConfig = null;
220
+ // Load global config
221
+ if (fs.existsSync(globalPath)) {
222
+ try {
223
+ const content = fs.readFileSync(globalPath, 'utf-8');
224
+ globalConfig = JSON.parse(content);
225
+ console.log(`${CLI_PREFIX} Loaded global config: ${globalPath}`);
226
+ }
227
+ catch (error) {
228
+ console.error(`${CLI_PREFIX} Failed to parse global config: ${globalPath}`, error);
229
+ }
230
+ }
231
+ // Load project config
232
+ const projectPath = fs.existsSync(projectOpencodePath) ? projectOpencodePath :
233
+ fs.existsSync(projectLocalPath) ? projectLocalPath : null;
234
+ if (projectPath) {
235
+ try {
236
+ const content = fs.readFileSync(projectPath, 'utf-8');
237
+ projectConfig = JSON.parse(content);
238
+ console.log(`${CLI_PREFIX} Loaded project config: ${projectPath}`);
239
+ }
240
+ catch (error) {
241
+ console.error(`${CLI_PREFIX} Failed to parse project config: ${projectPath}`, error);
242
+ }
243
+ }
244
+ // If no configs found
245
+ if (!globalConfig && !projectConfig) {
246
+ console.error(`${CLI_PREFIX} No configuration file found.`);
247
+ console.error(`${CLI_PREFIX} Checked locations:`);
248
+ console.error(` - ${globalPath}`);
249
+ console.error(` - ${projectOpencodePath}`);
250
+ console.error(` - ${projectLocalPath}`);
251
+ process.exit(1);
252
+ }
253
+ // Merge channels (project overrides global by name)
254
+ const globalChannels = globalConfig?.channels || [];
255
+ const projectChannels = projectConfig?.channels || [];
256
+ const mergedChannels = [...globalChannels];
257
+ for (const projectChannel of projectChannels) {
258
+ if (projectChannel.name) {
259
+ const existingIndex = mergedChannels.findIndex(c => c.name === projectChannel.name);
260
+ if (existingIndex >= 0) {
261
+ mergedChannels[existingIndex] = projectChannel;
262
+ continue;
263
+ }
264
+ }
265
+ mergedChannels.push(projectChannel);
266
+ }
267
+ return { channels: mergedChannels };
268
+ }
269
+ /**
270
+ * Main CLI entry point
271
+ */
272
+ async function main() {
273
+ const { command, options } = parseArgs();
274
+ if (command !== 'test') {
275
+ console.error(`${CLI_PREFIX} Unknown command: "${command || '(none)'}"`);
276
+ printUsage();
277
+ process.exit(1);
278
+ }
279
+ console.log(`${CLI_PREFIX} Starting channel test...`);
280
+ // Load configuration
281
+ const config = loadConfigForTesting();
282
+ if (config.channels.length === 0) {
283
+ console.error(`${CLI_PREFIX} No channels configured.`);
284
+ process.exit(1);
285
+ }
286
+ // Filter channels by name if specified
287
+ let channelsToTest = config.channels;
288
+ if (options.channelName) {
289
+ channelsToTest = config.channels.filter(c => c.name === options.channelName);
290
+ if (channelsToTest.length === 0) {
291
+ console.error(`${CLI_PREFIX} No channel found with name: "${options.channelName}"`);
292
+ console.error(`${CLI_PREFIX} Available channels:`);
293
+ config.channels.forEach(c => console.error(` - ${c.name || 'unnamed'}`));
294
+ process.exit(1);
295
+ }
296
+ }
297
+ console.log(`${CLI_PREFIX} Found ${channelsToTest.length} channel(s) to test`);
298
+ // Test each channel
299
+ let successCount = 0;
300
+ let failCount = 0;
301
+ for (const channel of channelsToTest) {
302
+ const success = await testChannel(channel, options);
303
+ if (success) {
304
+ successCount++;
305
+ }
306
+ else {
307
+ failCount++;
308
+ }
309
+ }
310
+ // Summary
311
+ console.log(`\n${CLI_PREFIX} Test Summary:`);
312
+ console.log(` Passed: ${successCount}`);
313
+ console.log(` Failed: ${failCount}`);
314
+ if (failCount > 0) {
315
+ process.exit(1);
316
+ }
317
+ }
318
+ // Run CLI
319
+ main().catch(error => {
320
+ console.error(`${CLI_PREFIX} Unexpected error:`, error);
321
+ process.exit(1);
322
+ });
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { NotificationConfig } from './types';
1
+ import type { NotificationConfig } from './types.js';
2
2
  /**
3
3
  * Load and merge notification configuration
4
4
  *
package/dist/config.js ADDED
@@ -0,0 +1,151 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ const SUPPORTED_CHANNEL_TYPES = ['smtp'];
5
+ const LOG_PREFIX = '[ul-opencode-event]';
6
+ const DEBUG_ENABLED = process.env.UL_OPENCODE_EVENT_DEBUG === '1' || process.env.UL_OPENCODE_EVENT_DEBUG === 'true';
7
+ /**
8
+ * Default configuration returned when no config files exist
9
+ */
10
+ const DEFAULT_CONFIG = {
11
+ channels: [],
12
+ };
13
+ /**
14
+ * Load and parse a JSON config file if it exists
15
+ */
16
+ function loadConfigFile(filePath) {
17
+ try {
18
+ if (!fs.existsSync(filePath)) {
19
+ return null;
20
+ }
21
+ const content = fs.readFileSync(filePath, 'utf-8');
22
+ const config = JSON.parse(content);
23
+ if (!Array.isArray(config.channels)) {
24
+ console.error(`${LOG_PREFIX} Invalid config format in ${filePath}: "channels" must be an array`);
25
+ return null;
26
+ }
27
+ if (DEBUG_ENABLED) {
28
+ console.info(`${LOG_PREFIX} Loaded config: ${filePath} (${config.channels.length} channels)`);
29
+ }
30
+ return config;
31
+ }
32
+ catch (error) {
33
+ console.error(`${LOG_PREFIX} Failed to parse config file: ${filePath}`, error);
34
+ return null;
35
+ }
36
+ }
37
+ /**
38
+ * Validate that all channel types are supported
39
+ * @throws Error if any channel has an unsupported type
40
+ */
41
+ function validateChannelTypes(channels) {
42
+ for (const channel of channels) {
43
+ if (!SUPPORTED_CHANNEL_TYPES.includes(channel.type)) {
44
+ throw new Error(`Unsupported channel type: "${channel.type}". Supported types: ${SUPPORTED_CHANNEL_TYPES.join(', ')}`);
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Deep merge channels by name
50
+ * - If project channel name matches global channel name, project overrides global
51
+ * - If no name match, project channels are appended to global channels
52
+ */
53
+ function mergeChannels(globalChannels, projectChannels) {
54
+ if (!globalChannels?.length && !projectChannels?.length) {
55
+ return [];
56
+ }
57
+ if (!globalChannels?.length) {
58
+ return projectChannels ?? [];
59
+ }
60
+ if (!projectChannels?.length) {
61
+ return globalChannels;
62
+ }
63
+ const result = [...globalChannels];
64
+ for (const projectChannel of projectChannels) {
65
+ const existingIndex = result.findIndex((c) => c.name !== undefined && c.name === projectChannel.name);
66
+ if (existingIndex >= 0) {
67
+ // Override global channel with project channel (same name)
68
+ result[existingIndex] = projectChannel;
69
+ }
70
+ else {
71
+ // Append project channel (no name match)
72
+ result.push(projectChannel);
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+ /**
78
+ * Filter channels to only include enabled ones
79
+ * A channel is enabled if enabled is true or undefined (defaults to true)
80
+ */
81
+ function filterEnabledChannels(channels) {
82
+ return channels.filter((channel) => channel.enabled !== false);
83
+ }
84
+ /**
85
+ * Get global config file path: ~/.config/opencode/ul-opencode-event.json
86
+ */
87
+ function getGlobalConfigPath() {
88
+ return path.join(os.homedir(), '.config', 'opencode', 'ul-opencode-event.json');
89
+ }
90
+ /**
91
+ * Get project config file path
92
+ * Priority: .opencode/ul-opencode-event.json > ./ul-opencode-event.json
93
+ */
94
+ function getProjectConfigPath() {
95
+ const opencodePath = path.join(process.cwd(), '.opencode', 'ul-opencode-event.json');
96
+ if (fs.existsSync(opencodePath)) {
97
+ return opencodePath;
98
+ }
99
+ const localPath = path.join(process.cwd(), 'ul-opencode-event.json');
100
+ if (fs.existsSync(localPath)) {
101
+ return localPath;
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Load and merge notification configuration
107
+ *
108
+ * Config locations (in order of priority):
109
+ * 1. Project-level: .opencode/ul-opencode-event.json or ./ul-opencode-event.json
110
+ * 2. Global: ~/.config/opencode/ul-opencode-event.json
111
+ *
112
+ * Merge strategy:
113
+ * - Channels are merged by `name` field
114
+ * - If names match, project channel overrides global channel
115
+ * - If no name match, project channels are appended to global channels
116
+ *
117
+ * Filtering:
118
+ * - Only returns channels where enabled is true (or undefined, which defaults to true)
119
+ * - Channels with enabled: false are excluded
120
+ *
121
+ * Validation:
122
+ * - Channel type must be 'smtp' (only supported type in phase 1)
123
+ * - Throws clear error for unsupported channel types
124
+ *
125
+ * @returns Merged and filtered configuration, or default config if no files exist
126
+ * @throws Error if any channel has an unsupported type
127
+ */
128
+ export function loadConfig() {
129
+ // Load global config
130
+ const globalConfigPath = getGlobalConfigPath();
131
+ const globalConfig = loadConfigFile(globalConfigPath);
132
+ // Load project config
133
+ const projectConfigPath = getProjectConfigPath();
134
+ const projectConfig = projectConfigPath ? loadConfigFile(projectConfigPath) : null;
135
+ // If no configs exist, return default
136
+ if (!globalConfig && !projectConfig) {
137
+ if (DEBUG_ENABLED) {
138
+ console.info(`${LOG_PREFIX} No config found. Checked: ${globalConfigPath}, ${path.join(process.cwd(), '.opencode', 'ul-opencode-event.json')}, ${path.join(process.cwd(), 'ul-opencode-event.json')}`);
139
+ }
140
+ return DEFAULT_CONFIG;
141
+ }
142
+ // Merge channels
143
+ const mergedChannels = mergeChannels(globalConfig?.channels, projectConfig?.channels);
144
+ // Validate channel types
145
+ validateChannelTypes(mergedChannels);
146
+ // Filter enabled channels
147
+ const enabledChannels = filterEnabledChannels(mergedChannels);
148
+ return {
149
+ channels: enabledChannels,
150
+ };
151
+ }
package/dist/email.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Channel } from './types';
1
+ import type { Channel } from './types.js';
2
2
  export interface EmailSender {
3
3
  send: (subject: string, body: string) => Promise<void>;
4
4
  }
package/dist/email.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Email sender module for SMTP-based notifications
3
+ */
4
+ import * as nodemailer from 'nodemailer';
5
+ /**
6
+ * Creates an email sender for the given SMTP channel
7
+ * @param channel - The channel configuration (must be SMTP type)
8
+ * @returns EmailSender instance or null if channel is invalid/disabled
9
+ */
10
+ export function createEmailSender(channel) {
11
+ // Type guard: return null for non-smtp channels
12
+ if (channel.type !== 'smtp') {
13
+ return null;
14
+ }
15
+ // Return null for disabled channels
16
+ if (channel.enabled === false) {
17
+ return null;
18
+ }
19
+ // Return null if no recipients configured
20
+ if (!channel.recipients || channel.recipients.length === 0) {
21
+ return null;
22
+ }
23
+ const config = channel.config;
24
+ const fromName = channel.name || 'Notification';
25
+ // Create nodemailer transport with SMTP config
26
+ const transport = nodemailer.createTransport({
27
+ host: config.host,
28
+ port: config.port,
29
+ secure: config.secure,
30
+ auth: {
31
+ user: config.auth.user,
32
+ pass: config.auth.pass,
33
+ },
34
+ });
35
+ // Set security options
36
+ transport.set('disableFileAccess', true);
37
+ transport.set('disableUrlAccess', true);
38
+ return {
39
+ async send(subject, body) {
40
+ try {
41
+ await transport.sendMail({
42
+ from: `"${fromName}" <${config.auth.user}>`,
43
+ to: channel.recipients.join(', '),
44
+ subject,
45
+ html: body,
46
+ text: body,
47
+ });
48
+ }
49
+ catch (error) {
50
+ // Silent failure: log error but don't throw
51
+ const timestamp = new Date().toISOString();
52
+ console.error(`[${timestamp}] [${fromName}] Failed to send email:`, error);
53
+ }
54
+ },
55
+ };
56
+ }
package/dist/handler.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Multi-channel event handler for notifications
3
3
  */
4
- import type { NotificationConfig, EventPayload, EventType } from './types';
4
+ import type { NotificationConfig, EventPayload, EventType } from './types.js';
5
5
  export interface EventHandler {
6
6
  handle: (eventType: EventType, payload: EventPayload) => Promise<void>;
7
7
  }